diff --git a/docs/administration-guide/07-segments-and-metrics.md b/docs/administration-guide/07-segments-and-metrics.md
index 312c5ed34d05696f431c7d6a56611fde9ad82490..e8f0f2ba0643c63121aefd6aeeb4744dc1ae8cfb 100644
--- a/docs/administration-guide/07-segments-and-metrics.md
+++ b/docs/administration-guide/07-segments-and-metrics.md
@@ -27,7 +27,7 @@ A custom metric is an easy way to refer to a computed number that you reference
 So, you create a custom metric in a very similar way to how you create segments: start by clicking on the **Add a Metric** link from a table’s detail view in the Admin Panel.
 ![Add metric](images/AddMetric.png)
 
-Here your presented with a slightly different version of the query builder, which only lets you select filters and aggregations. Filters are optional: a metric only requires an aggregation on a field. Note that you can use segments in the definition of metrics — pretty cool, right? Go ahead and select your filters, if any, and choose your aggregation. Give your metric a name and a description, and click **Save changes** when you’re done. Just like with segments, you can use the **Preview** button to see how your metric looks in the query builder before you save it.
+Here you're presented with a slightly different version of the query builder, which only lets you select filters and aggregations. Filters are optional: a metric only requires an aggregation on a field. Note that you can use segments in the definition of metrics — pretty cool, right? Go ahead and select your filters, if any, and choose your aggregation. Give your metric a name and a description, and click **Save changes** when you’re done. Just like with segments, you can use the **Preview** button to see how your metric looks in the query builder before you save it.
 ![abc](images/CreateMetric.png)
 
 Your new metric will now be available from the View dropdown in the query builder, under **Common Metrics**.
diff --git a/frontend/src/metabase-lib/lib/metadata/Field.js b/frontend/src/metabase-lib/lib/metadata/Field.js
index 26e81cea30dbf1c57b893183050a95ffddbfcf48..4bc93e7f9a2c95c340f00b41f0fc537bdc82af5c 100644
--- a/frontend/src/metabase-lib/lib/metadata/Field.js
+++ b/frontend/src/metabase-lib/lib/metadata/Field.js
@@ -8,6 +8,7 @@ import { FieldIDDimension } from "../Dimension";
 import { getFieldValues } from "metabase/lib/query/field";
 import {
     isDate,
+    isTime,
     isNumber,
     isNumeric,
     isBoolean,
@@ -41,6 +42,9 @@ export default class Field extends Base {
     isDate() {
         return isDate(this);
     }
+    isTime() {
+        return isTime(this);
+    }
     isNumber() {
         return isNumber(this);
     }
diff --git a/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx b/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx
index 575caead385eedf3f5d39f4956d71e315358fcc2..15a807c1f6425aa4ae7ff44d1c612fa58fbe2f36 100644
--- a/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx
+++ b/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx
@@ -92,7 +92,7 @@ export default class DatabaseSchedulingForm extends Component {
                         <div className="Form-offset mr4 mt4">
                             <div style={{maxWidth: 600}} className="border-bottom pb2">
                                 <p className="text-paragraph text-measure">
-                                  {t`To do some of its magic, Metabase needs to scan your database. We will also <em>re</em>scan it periodically to keep the metadata up-to-date. You can control when the periodic rescans happen below.`}
+                                  {t`To do some of its magic, Metabase needs to scan your database. We will also rescan it periodically to keep the metadata up-to-date. You can control when the periodic rescans happen below.`}
                                 </p>
                             </div>
 
diff --git a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
index 1d62fa8e7c365a4a122881fad9a39d74de414863..17b8cae7a3d766a11939d25f9e32eede6e115c4a 100644
--- a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
+++ b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
@@ -61,10 +61,10 @@ export default class DeleteDatabaseModal extends Component {
                         <p className="text-paragraph">{t`<strong>Just a heads up:</strong> without the Sample Dataset, the Query Builder tutorial won't work. You can always restore the Sample Dataset, but any questions you've saved using this data will be lost.`}</p>
                     }
                     <p className="text-paragraph">
-                      {t`All saved questions, metrics, and segments that rely on this database will be lost. <strong>This cannot be undone</strong>.`}
+                      {t`All saved questions, metrics, and segments that rely on this database will be lost.`} <strong>{t`This cannot be undone.`}</strong>
                     </p>
                     <p className="text-paragraph">
-                      {t`If you're sure, please type <strong>DELETE</strong> in this box:`}
+                      {t`If you're sure, please type`} <strong>{t`DELETE`}</strong> {t`in this box:`}
                     </p>
                     <input className="Form-input" type="text" onChange={(e) => this.setState({ confirmValue: e.target.value })} autoFocus />
                 </div>
diff --git a/frontend/src/metabase/app.js b/frontend/src/metabase/app.js
index 7449974772cc4805ec1f1e3f1e3cd119bb7b1009..fc84e2098ff6c05b1dc61c832d916745835b3507 100644
--- a/frontend/src/metabase/app.js
+++ b/frontend/src/metabase/app.js
@@ -3,6 +3,10 @@
 import 'babel-polyfill';
 import 'number-to-locale-string';
 
+// If enabled this monkeypatches `t` and `jt` to return blacked out
+// strings/elements to assist in finding untranslated strings.
+import "metabase/lib/i18n-debug";
+
 // make the i18n function "t" global so we don't have to import it in basically every file
 import { t, jt } from "c-3po";
 global.t = t;
diff --git a/frontend/src/metabase/components/ChannelSetupModal.jsx b/frontend/src/metabase/components/ChannelSetupModal.jsx
index af97fb7805ddaa5dcbc31c708b98952d0c5f3cf0..76fe11a30db41fa0bbf23790acc7b7034b6a2692 100644
--- a/frontend/src/metabase/components/ChannelSetupModal.jsx
+++ b/frontend/src/metabase/components/ChannelSetupModal.jsx
@@ -12,7 +12,7 @@ export default class ChannelSetupModal extends Component {
         user: PropTypes.object.isRequired,
         entityNamePlural: PropTypes.string.isRequired,
         channels: PropTypes.array,
-        fullPageModal: PropTypes.boolean,
+        fullPageModal: PropTypes.bool,
     };
 
     static defaultProps = {
@@ -36,4 +36,3 @@ export default class ChannelSetupModal extends Component {
         );
     }
 }
-
diff --git a/frontend/src/metabase/components/CheckBox.jsx b/frontend/src/metabase/components/CheckBox.jsx
index 83f30753d72a3e236894f19bfd280a4235ff85d3..eca9d88515e28e3faef2276f13433dc244caabfb 100644
--- a/frontend/src/metabase/components/CheckBox.jsx
+++ b/frontend/src/metabase/components/CheckBox.jsx
@@ -8,7 +8,7 @@ export default class CheckBox extends Component {
     static propTypes = {
         checked: PropTypes.bool,
         onChange: PropTypes.func,
-        color: PropTypes.oneOf(defaultColors),
+        color: PropTypes.oneOf(Object.keys(defaultColors)),
         size: PropTypes.number,  // TODO - this should probably be a concrete set of options
         padding: PropTypes.number// TODO - the component should pad itself properly based on the size
     };
diff --git a/frontend/src/metabase/components/SchedulePicker.jsx b/frontend/src/metabase/components/SchedulePicker.jsx
index 57e9846caebb1de20fcd5c40e3c6313f9b22fc7c..888aff1935d2c3d1285f87c01426f684928a525f 100644
--- a/frontend/src/metabase/components/SchedulePicker.jsx
+++ b/frontend/src/metabase/components/SchedulePicker.jsx
@@ -47,7 +47,7 @@ export default class SchedulePicker extends Component {
         schedule: PropTypes.object.isRequired,
         // TODO: hourly option?
         // available schedules, e.g. [ "daily", "weekly", "monthly"]
-        scheduleOptions: PropTypes.object.isRequired,
+        scheduleOptions: PropTypes.array.isRequired,
         // text before Daily/Weekly/Monthly... option
         textBeforeInterval: PropTypes.string,
         // text prepended to "12:00 PM PST, your Metabase timezone"
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index 2fcc0bc2d7ee2119e0e0aabc5ff3dc0887f698b9..a64e9b9174157ef0c0a814726f176914392a74b1 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -27,6 +27,10 @@ export var ICON_PATHS = {
         attrs: { fillRule: "evenodd" }
     },
     area: 'M31.154 28.846l.852.004V8.64l-1.15 2.138-6.818 6.37c-.13.122-9.148 1.622-9.148 1.622l-.545.096-.383.4-7.93 8.31-1.016 1.146 2.227.017 23.91.107L7.25 28.74l7.93-8.31 9.615-1.684 7.211-6.737v15.984a.855.855 0 0 1-.852.854zM0 28.74l11.79-13.362 11.788-3.369 8.077-8.07c.194-.193.351-.128.351.15V28.85L0 28.74z',
+    attachment: {
+        path: "M22.162 8.704c.029 8.782-.038 14.123-.194 15.926-.184 2.114-2.922 4.322-5.9 4.322-3.06 0-5.542-1.98-5.836-4.376-.294-2.392-.195-14.266.01-18.699.077-1.661 1.422-2.83 3.548-2.83 2.067 0 3.488 1.335 3.594 3.164.06 1.052.074 3.49.053 7.107-.006.928-.013 1.891-.023 3.072l-.023 2.527c-.006.824-.01 1.358-.01 1.718 0 1.547-.39 2.011-1.475 2.011-.804 0-1.202-.522-1.202-1.38V8.699a1.524 1.524 0 0 0-3.048 0v12.567c0 2.389 1.554 4.428 4.25 4.428 2.897 0 4.523-1.934 4.523-5.06 0-.348.003-.875.01-1.691l.022-2.526c.01-1.184.018-2.15.024-3.082.021-3.697.008-6.155-.058-7.3C20.227 2.592 17.469 0 13.79 0c-3.695 0-6.438 2.382-6.593 5.737-.213 4.613-.312 16.585.01 19.21C7.697 28.94 11.53 32 16.067 32c4.482 0 8.61-3.327 8.937-7.106.168-1.935.235-7.302.206-16.2a1.524 1.524 0 0 0-3.048.01z",
+        attrs: { fillRule: 'nonzero'}
+    },
     backArrow: 'M11.7416687,19.0096 L18.8461178,26.4181004 L14.2696969,30.568 L0.38960831,16.093881 L0,15.6875985 L0.49145276,15.241949 L14.6347557,1 L19.136,5.22693467 L11.3214393,13.096 L32,13.096 L32,19.0096 L11.7416687,19.0096 Z',
     bar: 'M2 23.467h6.4V32H2v-8.533zm10.667-12.8h6.4V32h-6.4V10.667zM23.333 0h6.4v32h-6.4V0z',
     beaker: 'M4.31736354,31.1631075 C3.93810558,30.6054137 3.89343681,29.6635358 4.20559962,29.0817181 L11.806982,14.9140486 L11.8069821,10.5816524 L10.7015144,10.4653256 C10.0309495,10.394763 9.48734928,9.78799739 9.48734928,9.12166999 L9.48734928,7.34972895 C9.48734928,6.67821106 10.0368737,6.13383825 10.7172248,6.13383825 L21.8462005,6.13383825 C22.525442,6.13383825 23.0760761,6.68340155 23.0760761,7.34972895 L23.0760761,9.12166999 C23.0760761,9.79318788 22.5250158,10.3375607 21.856025,10.3375607 L20.9787023,10.3375607 L20.9787024,14.9281806 L28.77277,29.0827118 C29.0983515,29.6739888 29.0709073,30.6193105 28.7174156,31.1846409 L28.852457,30.9686726 C28.4963041,31.538259 27.6541076,32 26.9865771,32 L6.10749779,32 C5.43315365,32 4.58248747,31.5529687 4.19978245,30.9902061 L4.31736354,31.1631075 Z M15.5771418,17.6040443 C16.5170398,17.6040443 17.2789777,16.8377777 17.2789777,15.89254 C17.2789777,14.9473023 16.5170398,14.1810358 15.5771418,14.1810358 C14.6372438,14.1810358 13.8753059,14.9473023 13.8753059,15.89254 C13.8753059,16.8377777 14.6372438,17.6040443 15.5771418,17.6040443 Z M16.5496195,12.8974079 C17.8587633,12.8974079 18.9200339,11.830108 18.9200339,10.5135268 C18.9200339,9.1969457 17.8587633,8.1296458 16.5496195,8.1296458 C15.2404758,8.1296458 14.1792052,9.1969457 14.1792052,10.5135268 C14.1792052,11.830108 15.2404758,12.8974079 16.5496195,12.8974079 Z M5.71098553,30.2209651 L10.9595331,20.5151267 C10.9595331,20.5151267 12.6834557,21.2672852 14.3734184,21.2672852 C16.0633811,21.2672852 16.8198616,19.2872624 17.588452,18.6901539 C18.3570425,18.0930453 19.9467191,17.1113296 19.9467191,17.1113296 L27.0506095,30.1110325 L5.71098553,30.2209651 Z M13.6608671,4.37817079 C14.4114211,4.37817079 15.0198654,3.78121712 15.0198654,3.04483745 C15.0198654,2.30845779 14.4114211,1.71150412 13.6608671,1.71150412 C12.9103132,1.71150412 12.3018689,2.30845779 12.3018689,3.04483745 C12.3018689,3.78121712 12.9103132,4.37817079 13.6608671,4.37817079 Z M17.9214578,2.45333328 C18.6119674,2.45333328 19.1717361,1.90413592 19.1717361,1.22666664 C19.1717361,0.549197362 18.6119674,0 17.9214578,0 C17.2309481,0 16.6711794,0.549197362 16.6711794,1.22666664 C16.6711794,1.90413592 17.2309481,2.45333328 17.9214578,2.45333328 Z',
diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js
index 245a18ecf467855b31051ee5cc25155863dd36c0..7ef945bcc72e2d72a8047a19f6a9bb9373ed1937 100644
--- a/frontend/src/metabase/lib/formatting.js
+++ b/frontend/src/metabase/lib/formatting.js
@@ -10,7 +10,7 @@ import ExternalLink from "metabase/components/ExternalLink.jsx";
 
 import { isDate, isNumber, isCoordinate, isLatitude, isLongitude } from "metabase/lib/schema_metadata";
 import { isa, TYPE } from "metabase/lib/types";
-import { parseTimestamp } from "metabase/lib/time";
+import { parseTimestamp,parseTime } from "metabase/lib/time";
 import { rangeForValue } from "metabase/lib/dataset";
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
 import { decimalCount } from "metabase/visualizations/lib/numeric";
@@ -191,6 +191,15 @@ export function formatTimeWithUnit(value: Value, unit: DatetimeUnit, options: Fo
     }
 }
 
+export function formatTimeValue(value: Value) {
+    let m = parseTime(value);
+    if (!m.isValid()){
+        return String(value);
+    } else {
+        return m.format("LT");
+    }
+}
+
 // https://github.com/angular/angular.js/blob/v1.6.3/src/ng/directive/input.js#L27
 const EMAIL_WHITELIST_REGEX = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
 
@@ -263,6 +272,8 @@ export function formatValue(value: Value, options: FormattingOptions = {}) {
         return formatUrl(value, options);
     } else if (column && isa(column.special_type, TYPE.Email)) {
         return formatEmail(value, options);
+    } else if (column && isa(column.base_type, TYPE.Time)) {
+        return formatTimeValue(value);
     } else if (column && column.unit != null) {
         return formatTimeWithUnit(value, column.unit, options);
     } else if (isDate(column) || moment.isDate(value) || moment.isMoment(value) || moment(value, ["YYYY-MM-DD'T'HH:mm:ss.SSSZ"], true).isValid()) {
diff --git a/frontend/src/metabase/lib/i18n-debug.js b/frontend/src/metabase/lib/i18n-debug.js
new file mode 100644
index 0000000000000000000000000000000000000000..6a82a4b020af13f4eb1b11b8f6b3ed94579c19a2
--- /dev/null
+++ b/frontend/src/metabase/lib/i18n-debug.js
@@ -0,0 +1,54 @@
+import React from "react";
+
+// If enabled this monkeypatches `t` and `jt` to return blacked out
+// strings/elements to assist in finding untranslated strings.
+//
+// Enable:
+//    localStorage["metabase-i18n-debug"] = true; window.location.reload()
+//
+// Disable:
+//    delete localStorage["metabase-i18n-debug"]; window.location.reload()
+//
+// Should be loaded before almost everything else.
+
+// special strings that need to be handled specially
+const SPECIAL_STRINGS = new Set([
+  // Expression editor aggregation names need to be unique for the parser
+  "Count",
+  "CumulativeCount",
+  "Sum",
+  "CumulativeSum",
+  "Distinct",
+  "StandardDeviation",
+  "Average",
+  "Min",
+  "Max"
+])
+
+export function enableTranslatedStringReplacement() {
+  const c3po = require("c-3po");
+  const _t = c3po.t;
+  const _jt = c3po.jt;
+  c3po.t = (...args) => {
+    const string = _t(...args);
+    if (SPECIAL_STRINGS.has(string)) {
+      return string.toUpperCase();
+    } else {
+      // divide by 2 because Unicode `FULL BLOCK` is quite wide
+      return new Array(Math.ceil(string.length / 2) + 1).join("â–ˆ");
+    }
+  }
+  // eslint-disable-next-line react/display-name
+  c3po.jt = (...args) => {
+    const elements = _jt(...args);
+    return (
+      <span style={{ backgroundColor: "currentcolor" }}>
+        {elements}
+      </span>
+    );
+  }
+}
+
+if (window.localStorage && window.localStorage["metabase-i18n-debug"]) {
+  enableTranslatedStringReplacement();
+}
diff --git a/frontend/src/metabase/lib/pulse.js b/frontend/src/metabase/lib/pulse.js
index e3f81da2a2240da1556e802b5c43490efad3392b..9b18a7ca2ea188164dbbd13c5328fcb224c40d0b 100644
--- a/frontend/src/metabase/lib/pulse.js
+++ b/frontend/src/metabase/lib/pulse.js
@@ -35,9 +35,41 @@ export function pulseIsValid(pulse, channelSpecs) {
     ) || false;
 }
 
+export function emailIsEnabled(pulse) {
+    return pulse.channels.filter(c => c.channel_type === "email" && c.enabled).length > 0;
+}
+
 export function cleanPulse(pulse, channelSpecs) {
     return {
         ...pulse,
         channels: pulse.channels.filter((c) => channelIsValid(c, channelSpecs && channelSpecs[c.channel_type]))
     };
 }
+
+export function getDefaultChannel(channelSpecs) {
+  // email is the first choice
+  if (channelSpecs.email.configured) {
+    return channelSpecs.email;
+  }
+  // otherwise just pick the first configured
+  for (const channelSpec of Object.values(channelSpecs)) {
+    if (channelSpec.configured) {
+      return channelSpec;
+    }
+  }
+}
+
+export function createChannel(channelSpec) {
+    const details = {};
+
+    return {
+        channel_type: channelSpec.type,
+        enabled: true,
+        recipients: [],
+        details: details,
+        schedule_type: channelSpec.schedules[0],
+        schedule_day: "mon",
+        schedule_hour: 8,
+        schedule_frame: "first"
+    };
+}
diff --git a/frontend/src/metabase/lib/schema_metadata.js b/frontend/src/metabase/lib/schema_metadata.js
index 4d9c4c1b5eb2a6a28ff4299eb2deea855c2e7f9a..4d25fe7aff3f2378a5d00efc3246f0ada1ea8804 100644
--- a/frontend/src/metabase/lib/schema_metadata.js
+++ b/frontend/src/metabase/lib/schema_metadata.js
@@ -119,6 +119,8 @@ export const isNumericBaseType = (field) => isa(field && field.base_type, TYPE.N
 // ZipCode, ID, etc derive from Number but should not be formatted as numbers
 export const isNumber = (field) => field && isNumericBaseType(field) && (field.special_type == null || field.special_type === TYPE.Number);
 
+export const isTime         = (field) => isa(field && field.base_type, TYPE.Time);
+
 export const isAddress      = (field) => isa(field && field.special_type, TYPE.Address);
 export const isState        = (field) => isa(field && field.special_type, TYPE.State);
 export const isCountry      = (field) => isa(field && field.special_type, TYPE.Country);
diff --git a/frontend/src/metabase/lib/time.js b/frontend/src/metabase/lib/time.js
index 102827e0446b18cc7ebcd2aad30014575606c051..aa17af36250e370b18ba0ce517a15560d73d884b 100644
--- a/frontend/src/metabase/lib/time.js
+++ b/frontend/src/metabase/lib/time.js
@@ -14,3 +14,13 @@ export function parseTimestamp(value, unit) {
         return moment.utc(value);
     }
 }
+
+export function parseTime(value) {
+    if (moment.isMoment(value)) {
+        return value;
+    } else if (typeof value === "string"){
+        return moment(value, ["HH:mm:SS.sssZZ", "HH:mm:SS.sss", "HH:mm:SS.sss", "HH:mm:SS", "HH:mm"])
+    } else {
+        return moment.utc(value);
+    }
+}
diff --git a/frontend/src/metabase/pulse/actions.js b/frontend/src/metabase/pulse/actions.js
index 72d1270716297946f3c070539aa3dd5400d750a3..f21ef27467714cf5779027ad10cd559e659b4c60 100644
--- a/frontend/src/metabase/pulse/actions.js
+++ b/frontend/src/metabase/pulse/actions.js
@@ -4,6 +4,9 @@ import { createThunkAction } from "metabase/lib/redux";
 import { normalize, schema } from "normalizr";
 
 import { PulseApi, CardApi, UserApi } from "metabase/services";
+import { formInputSelector } from "./selectors";
+
+import { getDefaultChannel, createChannel } from "metabase/lib/pulse";
 
 const card = new schema.Entity('card');
 const pulse = new schema.Entity('pulse');
@@ -37,10 +40,16 @@ export const setEditingPulse = createThunkAction(SET_EDITING_PULSE, function(id)
             } catch (e) {
             }
         }
+        // HACK: need a way to wait for form_input to finish loading
+        const channels = formInputSelector(getState()).channels ||
+            (await PulseApi.form_input()).channels;
+        const defaultChannelSpec = getDefaultChannel(channels);
         return {
             name: null,
             cards: [],
-            channels: [],
+            channels: defaultChannelSpec ?
+              [createChannel(defaultChannelSpec)] :
+              [],
             skip_if_empty: false,
         }
     };
@@ -93,7 +102,7 @@ export const fetchUsers = createThunkAction(FETCH_USERS, function() {
     };
 });
 
-export const fetchPulseFormInput = createThunkAction(FETCH_PULSE_FORM_INPUT, function(id) {
+export const fetchPulseFormInput = createThunkAction(FETCH_PULSE_FORM_INPUT, function() {
     return async function(dispatch, getState) {
         return await PulseApi.form_input();
     };
diff --git a/frontend/src/metabase/pulse/components/CardPicker.jsx b/frontend/src/metabase/pulse/components/CardPicker.jsx
index 035aa08c6900a614b8897b4871697b27a8f5ef88..c044b3ca961a1a3020634ceb6efdf67e9cf9840d 100644
--- a/frontend/src/metabase/pulse/components/CardPicker.jsx
+++ b/frontend/src/metabase/pulse/components/CardPicker.jsx
@@ -21,7 +21,8 @@ export default class CardPicker extends Component {
 
     static propTypes = {
         cardList: PropTypes.array.isRequired,
-        onChange: PropTypes.func.isRequired
+        onChange: PropTypes.func.isRequired,
+        attachmentsEnabled: PropTypes.bool,
     };
 
     componentWillUnmount() {
@@ -56,13 +57,14 @@ export default class CardPicker extends Component {
     }
 
     renderItem(card) {
+        const { attachmentsEnabled } = this.props;
         let error;
         try {
-            if (Query.isBareRows(card.dataset_query.query)) {
+            if (!attachmentsEnabled && Query.isBareRows(card.dataset_query.query)) {
                 error = t`Raw data cannot be included in pulses`;
             }
         } catch (e) {}
-        if (card.display === "pin_map" || card.display === "state" || card.display === "country") {
+        if (!attachmentsEnabled && (card.display === "pin_map" || card.display === "state" || card.display === "country")) {
             error = t`Maps cannot be included in pulses`;
         }
 
@@ -160,7 +162,7 @@ export default class CardPicker extends Component {
                     : collections ?
                         <CollectionList>
                             {collections.map(collection =>
-                                <CollectionListItem collection={collection} onClick={(e) => {
+                                <CollectionListItem key={collection.id} collection={collection} onClick={(e) => {
                                     this.setState({ collectionId: collection.id, isClicking: true });
                                 }}/>
                             )}
diff --git a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
index fd6a1aac9ac37b26fd8d5e9013bbaa62fab54204..652c2c55d5baca9d3d4f2dd4084f6103feaa530a 100644
--- a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
+++ b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
@@ -5,6 +5,10 @@ import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
+import Tooltip from "metabase/components/Tooltip.jsx";
+
+import { t } from "c-3po";
+import cx from "classnames";
 
 export default class PulseCardPreview extends Component {
     constructor(props, context) {
@@ -14,22 +18,83 @@ export default class PulseCardPreview extends Component {
     static propTypes = {
         card: PropTypes.object.isRequired,
         cardPreview: PropTypes.object,
+        onChange: PropTypes.func.isRequired,
         onRemove: PropTypes.func.isRequired,
         fetchPulseCardPreview: PropTypes.func.isRequired,
+        attachmentsEnabled: PropTypes.bool,
     };
 
     componentWillMount() {
         this.props.fetchPulseCardPreview(this.props.card.id);
     }
 
+    componentWillReceiveProps(nextProps) {
+        // if we can't render this card as a pulse, set include_csv = true
+        const unrenderablePulseCard = nextProps.cardPreview && nextProps.cardPreview.pulse_card_type == null;
+        const hasAttachment = nextProps.card.include_csv || nextProps.card.include_xls;
+        if (unrenderablePulseCard && !hasAttachment) {
+            nextProps.onChange({ ...nextProps.card, include_csv: true })
+        }
+    }
+
+    hasAttachment() {
+        const { card } = this.props;
+        return card.include_csv || card.include_xls;
+    }
+
+    toggleAttachment = () => {
+        const { card, onChange } = this.props;
+        if (this.hasAttachment()) {
+            onChange({ ...card, include_csv: false, include_xls: false })
+        } else {
+            onChange({ ...card, include_csv: true })
+        }
+    }
+
     render() {
-        let { cardPreview } = this.props;
+        let { cardPreview, attachmentsEnabled } = this.props;
+        const hasAttachment = this.hasAttachment();
+        const isAttachmentOnly = attachmentsEnabled && hasAttachment && cardPreview && cardPreview.pulse_card_type == null;
         return (
             <div className="flex relative flex-full">
-                <a className="text-grey-2 absolute" style={{ top: "15px", right: "15px" }} onClick={this.props.onRemove}>
-                    <Icon name="close" size={16} />
-                </a>
-                <div className="bordered rounded flex-full scroll-x" style={{ display: !cardPreview && "none" }} dangerouslySetInnerHTML={{__html: cardPreview && cardPreview.pulse_card_html}} />
+                <div className="absolute top right p2 text-grey-2">
+                    { attachmentsEnabled && !isAttachmentOnly &&
+                        <Tooltip tooltip={hasAttachment ? t`Remove attachment` : t`Attach file with results`}>
+                            <Icon
+                                name="attachment" size={18}
+                                className={cx("cursor-pointer py1 pr1 text-brand-hover", { "text-brand": this.hasAttachment() })}
+                                onClick={this.toggleAttachment}
+                            />
+                        </Tooltip>
+                    }
+                    <Icon
+                        name="close" size={18}
+                        className="cursor-pointer py1 pr1 text-brand-hover"
+                        onClick={this.props.onRemove}
+                    />
+                </div>
+                <div
+                    className="bordered rounded flex-full scroll-x"
+                    style={{ display: !cardPreview && "none" }}
+                >
+                    {/* Override backend rendering if pulse_card_type == null */}
+                    { cardPreview && cardPreview.pulse_card_type == null ?
+                      <RenderedPulseCardPreview href={cardPreview.pulse_card_url}>
+                        <RenderedPulseCardPreviewHeader>
+                          {cardPreview.pulse_card_name}
+                        </RenderedPulseCardPreviewHeader>
+                        <RenderedPulseCardPreviewMessage>
+                          { isAttachmentOnly ?
+                            t`This question will be added as a file attachment`
+                          :
+                            t`This question won't be included in your Pulse`
+                          }
+                        </RenderedPulseCardPreviewMessage>
+                      </RenderedPulseCardPreview>
+                    :
+                        <div dangerouslySetInnerHTML={{__html: cardPreview && cardPreview.pulse_card_html}} />
+                    }
+                </div>
                 { !cardPreview &&
                     <div className="flex-full flex align-center layout-centered pt1">
                         <LoadingSpinner className="inline-block" />
@@ -39,3 +104,58 @@ export default class PulseCardPreview extends Component {
         );
     }
 }
+
+// implements the same layout as in metabase/pulse/render.clj
+const RenderedPulseCardPreview = ({ href, children }) =>
+  <a
+    href={href}
+    style={{
+      fontFamily: 'Lato, "Helvetica Neue", Helvetica, Arial, sans-serif',
+      margin: 16,
+      marginBottom: 16,
+      display: "block",
+      textDecoration: "none"
+    }}
+    target="_blank"
+  >
+    {children}
+  </a>
+
+RenderedPulseCardPreview.propTypes = {
+  href: PropTypes.string,
+  children: PropTypes.node
+}
+
+// implements the same layout as in metabase/pulse/render.clj
+const RenderedPulseCardPreviewHeader = ({ children }) =>
+    <table style={{ marginBottom: 8, width: "100%" }}>
+      <tbody>
+        <tr>
+          <td>
+            <span style={{
+              fontFamily: 'Lato, "Helvetica Neue", Helvetica, Arial, sans-serif',
+              fontSize: 16,
+              fontWeight: 700,
+              color: "rgb(57,67,64)",
+              textDecoration: "none"
+            }}>
+              {children}
+            </span>
+          </td>
+          <td style={{ textAlign: "right" }}></td>
+        </tr>
+      </tbody>
+    </table>
+
+RenderedPulseCardPreviewHeader.propTypes = {
+  children: PropTypes.node
+}
+
+const RenderedPulseCardPreviewMessage = ({ children }) =>
+  <div className="text-grey-4">
+    {children}
+  </div>
+
+RenderedPulseCardPreviewMessage.propTypes = {
+  children: PropTypes.node
+}
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx
index 0ab0acfd2d862bbd793ad7f287953d1fe444eff6..4bd25ceb0e8e090badfa8bc953e8537ca4e917d5 100644
--- a/frontend/src/metabase/pulse/components/PulseEdit.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx
@@ -17,7 +17,7 @@ import ModalContent from "metabase/components/ModalContent.jsx";
 import DeleteModalWithConfirm from "metabase/components/DeleteModalWithConfirm.jsx";
 
 
-import { pulseIsValid, cleanPulse } from "metabase/lib/pulse";
+import { pulseIsValid, cleanPulse, emailIsEnabled } from "metabase/lib/pulse";
 
 import _ from "underscore";
 import cx from "classnames";
@@ -75,25 +75,21 @@ export default class PulseEdit extends Component {
         this.props.updateEditingPulse(pulse);
     }
 
-    isValid() {
-        let { pulse } = this.props;
-        return pulse.name && pulse.cards.length && pulse.channels.length > 0 && pulse.channels.filter((c) => this.channelIsValid(c)).length > 0;
-    }
-
     getConfirmItems() {
-        return this.props.pulse.channels.map(c =>
+        return this.props.pulse.channels.map((c, index) =>
             c.channel_type === "email" ?
-                <span>{jt`This pulse will no longer be emailed to ${<strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong>} ${<strong>{c.schedule_type}</strong>}`}.</span>
+                <span key={index}>{jt`This pulse will no longer be emailed to ${<strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong>} ${<strong>{c.schedule_type}</strong>}`}.</span>
             : c.channel_type === "slack" ?
-                <span>{jt`Slack channel ${<strong>{c.details && c.details.channel}</strong>} will no longer get this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
+                <span key={index}>{jt`Slack channel ${<strong>{c.details && c.details.channel}</strong>} will no longer get this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
             :
-                <span>{jt`Channel ${<strong>{c.channel_type}</strong>} will no longer receive this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
+                <span key={index}>{jt`Channel ${<strong>{c.channel_type}</strong>} will no longer receive this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
         );
     }
 
     render() {
-        let { pulse, formInput } = this.props;
-        let isValid = pulseIsValid(pulse, formInput.channels);
+        const { pulse, formInput } = this.props;
+        const isValid = pulseIsValid(pulse, formInput.channels);
+        const attachmentsEnabled = emailIsEnabled(pulse);
         return (
             <div className="PulseEdit">
                 <div className="PulseEdit-header flex align-center border-bottom py3">
@@ -117,7 +113,7 @@ export default class PulseEdit extends Component {
                 </div>
                 <div className="PulseEdit-content pt2 pb4">
                     <PulseEditName {...this.props} setPulse={this.setPulse} />
-                    <PulseEditCards {...this.props} setPulse={this.setPulse} />
+                    <PulseEditCards {...this.props} setPulse={this.setPulse} attachmentsEnabled={attachmentsEnabled} />
                     <div className="py1 mb4">
                         <h2 className="mb3">Where should this data go?</h2>
                         <PulseEditChannels {...this.props} setPulse={this.setPulse} pulseIsValid={isValid} />
diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
index 521bd45c37347b1492d51f88c93ba1b91889e974..6035c59c4c6f3732a90a2782d6830d1e23cdd1be 100644
--- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
@@ -2,6 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { t } from 'c-3po';
+import cx from "classnames";
 
 import CardPicker from "./CardPicker.jsx";
 import PulseCardPreview from "./PulseCardPreview.jsx";
@@ -24,16 +25,21 @@ export default class PulseEditCards extends Component {
         cards: PropTypes.object.isRequired,
         cardList: PropTypes.array.isRequired,
         fetchPulseCardPreview: PropTypes.func.isRequired,
-        setPulse: PropTypes.func.isRequired
+        setPulse: PropTypes.func.isRequired,
+        attachmentsEnabled: PropTypes.bool,
     };
     static defaultProps = {};
 
-    setCard(index, cardId) {
+    setCard(index, card) {
         let { pulse } = this.props;
         this.props.setPulse({
             ...pulse,
-            cards: [...pulse.cards.slice(0, index), { id: cardId }, ...pulse.cards.slice(index + 1)]
+            cards: [...pulse.cards.slice(0, index), card, ...pulse.cards.slice(index + 1)]
         });
+    }
+
+    addCard(index, cardId) {
+        this.setCard(index, { id: cardId })
 
         MetabaseAnalytics.trackEvent((this.props.pulseId) ? "PulseEdit" : "PulseCreate", "AddCard", index);
     }
@@ -48,41 +54,52 @@ export default class PulseEditCards extends Component {
         MetabaseAnalytics.trackEvent((this.props.pulseId) ? "PulseEdit" : "PulseCreate", "RemoveCard", index);
     }
 
-    getWarnings(cardPreview, showSoftLimitWarning) {
-        let warnings = [];
+    getNotices(card, cardPreview, index) {
+        const showSoftLimitWarning = index === SOFT_LIMIT;
+        let notices = [];
+        const hasAttachment = this.props.attachmentsEnabled && card && (card.include_csv || card.include_xls);
+        if (hasAttachment) {
+            notices.push({
+                head: t`Attachment`,
+                body: <AttachmentWidget card={card} onChange={(card) => this.setCard(index, card)} />
+            });
+        }
         if (cardPreview) {
-            if (cardPreview.pulse_card_type === "bar" && cardPreview.row_count > 10) {
-                warnings.push({
-                    head: t`Heads up`,
-                    body: t`This is a large table and we'll have to crop it to use it in a pulse. The max size that can be displayed is 2 columns and 10 rows.`
-                });
-            }
-            if (cardPreview.pulse_card_type == null) {
-                warnings.push({
+            if (cardPreview.pulse_card_type == null && !hasAttachment) {
+                notices.push({
+                    type: "warning",
                     head: t`Heads up`,
-                    body: t`We are unable to display this card in a pulse`
+                    body: t`Raw data questions can only be included as email attachments`
                 });
             }
         }
         if (showSoftLimitWarning) {
-            warnings.push({
+            notices.push({
+                type: "warning",
                 head: t`Looks like this pulse is getting big`,
                 body: t`We recommend keeping pulses small and focused to help keep them digestable and useful to the whole team.`
             });
         }
-        return warnings;
+        return notices;
     }
 
-    renderCardWarnings(card, index) {
+    renderCardNotices(card, index) {
         let cardPreview = card && this.props.cardPreviews[card.id];
-        let warnings = this.getWarnings(cardPreview, index === SOFT_LIMIT);
-        if (warnings.length > 0) {
+        let notices = this.getNotices(card, cardPreview, index);
+        if (notices.length > 0) {
             return (
                 <div className="absolute" style={{ width: 400, marginLeft: 420 }}>
-                    {warnings.map(warning =>
-                        <div className="text-gold border-gold border-left mt1 mb2 ml3 pl3" style={{ borderWidth: 3 }}>
-                            <h3 className="mb1">{warning.head}</h3>
-                            <div className="h4">{warning.body}</div>
+                    {notices.map((notice, index) =>
+                        <div
+                            key={index}
+                            className={cx("border-left mt1 mb2 ml3 pl3", {
+                              "text-gold border-gold": notice.type === "warning",
+                              "border-brand":          notice.type !== "warning"
+                            })}
+                            style={{ borderWidth: 3 }}
+                        >
+                            <h3 className="mb1">{notice.head}</h3>
+                            <div className="h4">{notice.body}</div>
                         </div>
                     )}
                 </div>
@@ -113,17 +130,20 @@ export default class PulseEditCards extends Component {
                                         <PulseCardPreview
                                             card={card}
                                             cardPreview={cardPreviews[card.id]}
+                                            onChange={this.setCard.bind(this, index)}
                                             onRemove={this.removeCard.bind(this, index)}
                                             fetchPulseCardPreview={this.props.fetchPulseCardPreview}
+                                            attachmentsEnabled={this.props.attachmentsEnabled}
                                         />
                                     :
                                         <CardPicker
                                             cardList={cardList}
-                                            onChange={this.setCard.bind(this, index)}
+                                            onChange={this.addCard.bind(this, index)}
+                                            attachmentsEnabled={this.props.attachmentsEnabled}
                                         />
                                     }
                                 </div>
-                                {this.renderCardWarnings(card, index)}
+                                {this.renderCardNotices(card, index)}
                             </div>
                         </li>
                     )}
@@ -132,3 +152,29 @@ export default class PulseEditCards extends Component {
         );
     }
 }
+
+const ATTACHMENT_TYPES = ["csv", "xls"];
+
+const AttachmentWidget = ({ card, onChange }) =>
+    <div>
+        { ATTACHMENT_TYPES.map(type =>
+            <span
+                key={type}
+                className={cx("text-brand-hover cursor-pointer mr1", { "text-brand": card["include_"+type] })}
+                onClick={() => {
+                    const newCard = { ...card }
+                    for (const attachmentType of ATTACHMENT_TYPES) {
+                      newCard["include_" + attachmentType] = type === attachmentType;
+                    }
+                    onChange(newCard)
+                }}
+            >
+                {"." + type}
+            </span>
+        )}
+    </div>
+
+AttachmentWidget.propTypes = {
+    card: PropTypes.object.isRequired,
+    onChange: PropTypes.func.isRequired
+}
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index 02628f181e134c4b8f6bf7fc60d71f88b4a8f852..85c4f92b1eac256a5c0c000379473e2c4db206e8 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -16,7 +16,7 @@ import ChannelSetupMessage from "metabase/components/ChannelSetupMessage";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-import { channelIsValid } from "metabase/lib/pulse";
+import { channelIsValid, createChannel } from "metabase/lib/pulse";
 
 import cx from "classnames";
 
@@ -45,7 +45,7 @@ export default class PulseEditChannels extends Component {
         userList: PropTypes.array.isRequired,
         setPulse: PropTypes.func.isRequired,
         testPulse: PropTypes.func,
-        cardPreviews: PropTypes.array,
+        cardPreviews: PropTypes.object,
         hideSchedulePicker: PropTypes.bool,
         emailRecipientText: PropTypes.string
     };
@@ -59,27 +59,7 @@ export default class PulseEditChannels extends Component {
             return;
         }
 
-        let details = {};
-        // if (channelSpec.fields) {
-        //     for (let field of channelSpec.fields) {
-        //         if (field.required) {
-        //             if (field.type === "select") {
-        //                 details[field.name] = field.options[0];
-        //             }
-        //         }
-        //     }
-        // }
-
-        let channel = {
-            channel_type: type,
-            enabled: true,
-            recipients: [],
-            details: details,
-            schedule_type: channelSpec.schedules[0],
-            schedule_day: "mon",
-            schedule_hour: 8,
-            schedule_frame: "first"
-        };
+        let channel = createChannel(channelSpec);
 
         this.props.setPulse({ ...pulse, channels: pulse.channels.concat(channel) });
 
@@ -184,6 +164,7 @@ export default class PulseEditChannels extends Component {
                         <div className="h4 text-bold mb1">{ this.props.emailRecipientText || "To:" }</div>
                         <RecipientPicker
                             isNewPulse={this.props.pulseId === undefined}
+                            autoFocus={!!this.props.pulse.name}
                             recipients={channel.recipients}
                             recipientTypes={channelSpec.recipients}
                             users={this.props.userList}
diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx
index a157c3b8642f96c51514b98bc66df0c018b8c095..fab914314065e68452324eb7d7df34997c794538 100644
--- a/frontend/src/metabase/pulse/components/PulseListItem.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { jt, t } from 'c-3po';
 
 import cx from "classnames";
 
@@ -29,12 +29,13 @@ export default class PulseListItem extends Component {
     render() {
         let { pulse, formInput, user } = this.props;
 
+        const creator = <span className="text-bold">{pulse.creator && pulse.creator.common_name}</span>;
         return (
             <div ref="pulseListItem" className={cx("PulseListItem bordered rounded mb2 pt3", {"PulseListItem--focused": this.props.scrollTo})}>
                 <div className="flex px4 mb2">
                     <div>
                         <h2 className="mb1">{pulse.name}</h2>
-                        <span>Pulse by <span className="text-bold">{pulse.creator && pulse.creator.common_name}</span></span>
+                        <span>{jt`Pulse by ${creator}`}</span>
                     </div>
                     { !pulse.read_only &&
                         <div className="flex-align-right">
diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
index bfd2acc0d4127dccb5ca320d96f94b7e1f5425b0..dbe0c34567132be41e20ff4fea5d6828384a9bfb 100644
--- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx
+++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
@@ -35,7 +35,7 @@ export default class RecipientPicker extends Component {
             inputValue: "",
             filteredUsers: [],
             selectedUserID: null,
-            focused: props.recipients.length === 0
+            focused: props.autoFocus && props.recipients.length === 0
         };
     }
 
@@ -47,10 +47,12 @@ export default class RecipientPicker extends Component {
         users: PropTypes.array,
         isNewPulse: PropTypes.bool.isRequired,
         onRecipientsChange: PropTypes.func.isRequired,
+        autoFocus: PropTypes.bool,
     };
 
     static defaultProps = {
-        recipientTypes: ["user", "email"]
+        recipientTypes: ["user", "email"],
+        autoFocus: true
     };
 
     setInputValue(inputValue) {
diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js
index f3be5bd7898d29f8e5e123ff4118d888c3c58f97..f7c24e3e648b0522c16b945b9a79ee893480010e 100644
--- a/frontend/src/metabase/query_builder/actions.js
+++ b/frontend/src/metabase/query_builder/actions.js
@@ -393,9 +393,11 @@ export const loadMetadataForCard = createThunkAction(LOAD_METADATA_FOR_CARD, (ca
                 await dispatch(loadTableMetadata(singleQuery.tableId()));
             }
 
-            if (singleQuery instanceof NativeQuery && singleQuery.databaseId() != null) {
-                await dispatch(loadDatabaseFields(singleQuery.databaseId()));
-            }
+            // NOTE Atte Keinänen 1/29/18:
+            // For native queries we don't normally know which table(s) we are working on.
+            // We could load all tables of the current database but historically that has caused
+            // major performance problems with users having large databases.
+            // Now components needing table metadata fetch it on-demand.
         }
 
         if (query) {
diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx
index 74109c4cd764fb0be5de06593c906af2cf6f8d33..3777e9de0a76a161038a3685fdbd3fb0e20d3514 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx
@@ -1,6 +1,8 @@
 import React, { Component } from "react";
+import { connect } from "react-redux";
 import PropTypes from "prop-types";
 import { t } from 'c-3po';
+import cx from 'classnames'
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import AccordianList from "metabase/components/AccordianList.jsx";
@@ -9,50 +11,103 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"
 import { isQueryable } from 'metabase/lib/table';
 import { titleize, humanize } from 'metabase/lib/formatting';
 
-import _ from "underscore";
+import { fetchTableMetadata } from "metabase/redux/metadata";
+import { getMetadata } from "metabase/selectors/metadata";
 
-export default class DataSelector extends Component {
+import _ from "underscore";
 
-    constructor(props) {
-        super()
-        this.state = {
-            databases: null,
-            selectedSchema: null,
-            showTablePicker: true,
-            showSegmentPicker: props.segments && props.segments.length > 0
-        }
+// chooses a database
+const DATABASE_STEP = 'DATABASE';
+// chooses a database and a schema inside that database
+const DATABASE_SCHEMA_STEP = 'DATABASE_SCHEMA';
+// chooses a schema (given that a database has already been selected)
+const SCHEMA_STEP = 'SCHEMA';
+// chooses a database and a schema and provides additional "Segments" option for jumping to SEGMENT_STEP
+const SCHEMA_AND_SEGMENTS_STEP = 'SCHEMA_AND_SEGMENTS';
+// chooses a table (database has already been selected)
+const TABLE_STEP = 'TABLE';
+// chooses a table field (table has already been selected)
+const FIELD_STEP = 'FIELD';
+// shows either table or segment list depending on which one is selected
+const SEGMENT_OR_TABLE_STEP = 'SEGMENT_OR_TABLE_STEP';
+
+export const SchemaTableAndSegmentDataSelector = (props) =>
+    <DataSelector
+        steps={[SCHEMA_AND_SEGMENTS_STEP, SEGMENT_OR_TABLE_STEP]}
+        getTriggerElementContent={SchemaAndSegmentTriggerContent}
+        {...props}
+    />
+const SchemaAndSegmentTriggerContent = ({ selectedTable, selectedSegment }) => {
+    if (selectedTable) {
+        return  <span className="text-grey no-decoration">{selectedTable.display_name || selectedTable.name}</span>;
+    } else if (selectedSegment) {
+        return <span className="text-grey no-decoration">{selectedSegment.name}</span>;
+    } else {
+        return <span className="text-grey-4 no-decoration">{t`Pick a segment or table`}</span>;
     }
+}
 
-    static propTypes = {
-        datasetQuery: PropTypes.object.isRequired,
-        databases: PropTypes.array.isRequired,
-        tables: PropTypes.array,
-        segments: PropTypes.array,
-        disabledTableIds: PropTypes.array,
-        disabledSegmentIds: PropTypes.array,
-        setDatabaseFn: PropTypes.func.isRequired,
-        setSourceTableFn: PropTypes.func,
-        setSourceSegmentFn: PropTypes.func,
-        isInitiallyOpen: PropTypes.bool
-    };
-
-    static defaultProps = {
-        isInitiallyOpen: false,
-        includeTables: false
-    };
-
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
-        if (this.props.databases.length === 1 && !this.props.segments) {
-            setTimeout(() => this.onChangeDatabase(0));
-        }
+export const DatabaseDataSelector = (props) =>
+    <DataSelector
+        steps={[DATABASE_STEP]}
+        getTriggerElementContent={DatabaseTriggerContent}
+        {...props}
+    />
+const DatabaseTriggerContent = ({ selectedDatabase }) =>
+    selectedDatabase
+        ? <span className="text-grey no-decoration">{selectedDatabase.name}</span>
+        : <span className="text-grey-4 no-decoration">{t`Select a database`}</span>
+
+export const SchemaTableAndFieldDataSelector = (props) =>
+    <DataSelector
+        steps={[SCHEMA_STEP, TABLE_STEP, FIELD_STEP]}
+        getTriggerElementContent={FieldTriggerContent}
+        triggerIconSize={12}
+        renderAsSelect={true}
+        {...props}
+    />
+const FieldTriggerContent = ({ selectedDatabase, selectedField }) => {
+    if (!selectedField || !selectedField.table) {
+        return <span className="flex-full text-grey-4 no-decoration">{t`Select...`}</span>
+    } else {
+        const hasMultipleSchemas = selectedDatabase && _.uniq(selectedDatabase.tables, (t) => t.schema).length > 1;
+        return (
+            <div className="flex-full cursor-pointer">
+                <div className="h6 text-bold text-uppercase text-grey-2">
+                    {hasMultipleSchemas && (selectedField.table.schema + " > ")}{selectedField.table.display_name}
+                </div>
+                <div className="h4 text-bold text-default">{selectedField.display_name}</div>
+            </div>
+        )
     }
+}
 
-    componentWillReceiveProps(newProps) {
-        let tableId = newProps.datasetQuery.query && newProps.datasetQuery.query.source_table;
-        let selectedSchema;
+export const DatabaseSchemaAndTableDataSelector = (props) =>
+    <DataSelector
+        steps={[DATABASE_SCHEMA_STEP, TABLE_STEP]}
+        getTriggerElementContent={TableTriggerContent}
+        {...props}
+    />
+export const SchemaAndTableDataSelector = (props) =>
+    <DataSelector
+        steps={[SCHEMA_STEP, TABLE_STEP]}
+        getTriggerElementContent={TableTriggerContent}
+        {...props}
+    />
+const TableTriggerContent = ({ selectedTable }) =>
+    selectedTable
+        ? <span className="text-grey no-decoration">{selectedTable.display_name || selectedTable.name}</span>
+        : <span className="text-grey-4 no-decoration">{t`Select a table`}</span>
+
+@connect(state => ({metadata: getMetadata(state)}), { fetchTableMetadata })
+export default class DataSelector extends Component {
+    constructor(props) {
+        super();
+
+        let selectedSchema, selectedTable;
+        let selectedDatabaseId = props.selectedDatabaseId;
         // augment databases with schemas
-        let databases = newProps.databases && newProps.databases.map(database => {
+        const databases = props.databases && props.databases.map(database => {
             let schemas = {};
             for (let table of database.tables.filter(isQueryable)) {
                 let name = table.schema || "";
@@ -62,8 +117,10 @@ export default class DataSelector extends Component {
                     tables: []
                 }
                 schemas[name].tables.push(table);
-                if (table.id === tableId) {
+                if (props.selectedTableId && table.id === props.selectedTableId) {
                     selectedSchema = schemas[name];
+                    selectedDatabaseId = selectedSchema.database.id;
+                    selectedTable = table;
                 }
             }
             schemas = Object.values(schemas);
@@ -76,50 +133,131 @@ export default class DataSelector extends Component {
                 schemas: schemas.sort((a, b) => a.name.localeCompare(b.name))
             };
         });
-        this.setState({ databases });
-        if (selectedSchema != undefined) {
-            this.setState({ selectedSchema,  })
+
+        const selectedDatabase = selectedDatabaseId ? databases.find(db => db.id === selectedDatabaseId) : null;
+        const hasMultipleSchemas = selectedDatabase && _.uniq(selectedDatabase.tables, (t) => t.schema).length > 1;
+
+        // remove the schema step if a database is already selected and the database does not have more than one schema.
+        let steps = [...props.steps]
+        if (selectedDatabase && !hasMultipleSchemas && steps.includes(SCHEMA_STEP)) {
+            steps.splice(props.steps.indexOf(SCHEMA_STEP), 1);
+            selectedSchema = selectedDatabase.schemas[0];
         }
+
+        // if a db is selected but schema isn't, default to the first schema
+        selectedSchema = selectedSchema || (selectedDatabase && selectedDatabase.schemas[0]);
+
+        const selectedSegmentId = props.selectedSegmentId
+        const selectedSegment = selectedSegmentId ? props.segments.find(segment => segment.id === selectedSegmentId) : null;
+        const selectedField = props.selectedFieldId ? props.metadata.fields[props.selectedFieldId] : null
+
+        this.state = {
+            databases,
+            selectedDatabase,
+            selectedSchema,
+            selectedTable,
+            selectedSegment,
+            selectedField,
+            activeStep: null,
+            steps,
+            isLoading: false,
+        };
     }
 
-    onChangeTable = (item) => {
-        if (item.table != null) {
-            this.props.setSourceTableFn(item.table.id);
-        } else if (item.database != null) {
-            this.props.setDatabaseFn(item.database.id);
+    static propTypes = {
+        selectedDatabaseId: PropTypes.number,
+        selectedSchemaId: PropTypes.number,
+        selectedTableId: PropTypes.number,
+        selectedFieldId: PropTypes.number,
+        selectedSegmentId: PropTypes.number,
+        databases: PropTypes.array.isRequired,
+        segments: PropTypes.array,
+        disabledTableIds: PropTypes.array,
+        disabledSegmentIds: PropTypes.array,
+        setDatabaseFn: PropTypes.func,
+        setFieldFn: PropTypes.func,
+        setSourceTableFn: PropTypes.func,
+        setSourceSegmentFn: PropTypes.func,
+        isInitiallyOpen: PropTypes.bool,
+        renderAsSelect: PropTypes.bool,
+    };
+
+    static defaultProps = {
+        isInitiallyOpen: false,
+        renderAsSelect: false,
+    };
+
+    componentWillMount() {
+        const useOnlyAvailableDatabase =
+            !this.props.selectedDatabaseId && this.props.databases.length === 1 && !this.props.segments
+        if (useOnlyAvailableDatabase) {
+            setTimeout(() => this.onChangeDatabase(0));
         }
-        this.refs.popover.toggle();
+
+        this.hydrateActiveStep();
     }
 
-    onChangeSegment = (item) => {
-        if (item.segment != null) {
-            this.props.setSourceSegmentFn(item.segment.id);
+    hydrateActiveStep() {
+        if (this.props.selectedFieldId) {
+            this.switchToStep(FIELD_STEP);
+        } else if (this.props.selectedSegmentId) {
+            this.switchToStep(SEGMENT_OR_TABLE_STEP);
+        } else if (this.props.selectedTableId) {
+            if (this.props.segments) {
+                this.switchToStep(SEGMENT_OR_TABLE_STEP);
+            } else {
+                this.switchToStep(TABLE_STEP);
+            }
+        } else {
+            let firstStep = this.state.steps[0];
+            this.switchToStep(firstStep)
         }
+    }
 
-        this.refs.popover.toggle();
+    nextStep = (stateChange = {}) => {
+        let activeStepIndex = this.state.steps.indexOf(this.state.activeStep);
+        if (activeStepIndex + 1 >= this.state.steps.length) {
+            this.setState(stateChange)
+            this.refs.popover.toggle();
+        } else {
+            const nextStep = this.state.steps[activeStepIndex + 1]
+            this.switchToStep(nextStep, stateChange);
+        }
     }
+    
+    switchToStep = async (stepName, stateChange = {}) => {
+        const updatedState =  { ...this.state, ...stateChange, activeStep: stepName }
+
+        const loadersForSteps = {
+            [FIELD_STEP]: () => updatedState.selectedTable && this.props.fetchTableMetadata(updatedState.selectedTable.id)
+        }
+
+        if (loadersForSteps[stepName]) {
+            this.setState({ ...updatedState, isLoading: true });
+            await loadersForSteps[stepName]();
+        }
 
-    onChangeSchema = (schema) => {
         this.setState({
-            selectedSchema: schema,
-            showTablePicker: true
+            ...updatedState,
+            isLoading: false
         });
     }
 
-    onChangeSegmentSection = () => {
-        this.setState({
-            showSegmentPicker: true
-        });
+    hasPreviousStep = () => {
+        return !!this.state.steps[this.state.steps.indexOf(this.state.activeStep) - 1];
+    }
+
+    hasAdjacentStep = () => {
+        return !!this.state.steps[this.state.steps.indexOf(this.state.activeStep) + 1];
     }
 
     onBack = () => {
-        this.setState({
-            showTablePicker: false,
-            showSegmentPicker: false
-        });
+        if (!this.hasPreviousStep()) { return; }
+        const previousStep = this.state.steps[this.state.steps.indexOf(this.state.activeStep) - 1];
+        this.switchToStep(previousStep)
     }
 
-    onChangeDatabase = (index) => {
+    onChangeDatabase = (index, schemaInSameStep) => {
         let database = this.state.databases[index];
         let schema = database && (database.schemas.length > 1 ? null : database.schemas[0]);
         if (database && database.tables.length === 0) {
@@ -129,67 +267,265 @@ export default class DataSelector extends Component {
                 tables: []
             };
         }
-        this.setState({
-            selectedSchema: schema,
-            showTablePicker: !!schema
-        });
+        const stateChange = {
+            selectedDatabase: database,
+            selectedSchema: schema
+        };
+
+        this.props.setDatabaseFn && this.props.setDatabaseFn(database.id);
+
+        if (schemaInSameStep) {
+            if (database.schemas.length > 1) {
+                this.setState(stateChange)
+            } else {
+                this.nextStep(stateChange)
+            }
+        } else {
+            this.nextStep(stateChange)
+        }
     }
 
-    getSegmentId() {
-        return this.props.datasetQuery.segment;
+    onChangeSchema = (schema) => {
+        this.nextStep({selectedSchema: schema});
     }
 
-    getDatabaseId() {
-        return this.props.datasetQuery.database;
+    onChangeTable = (item) => {
+        if (item.table != null) {
+            this.props.setSourceTableFn && this.props.setSourceTableFn(item.table.id);
+            this.nextStep({selectedTable: item.table});
+        }
     }
 
-    getTableId() {
-        return this.props.datasetQuery.query && this.props.datasetQuery.query.source_table;
+    onChangeField = (item) => {
+        if (item.field != null) {
+            this.props.setFieldFn && this.props.setFieldFn(item.field.id);
+            this.nextStep({selectedField: item.field});
+        }
     }
 
-    renderDatabasePicker = ({ maxHeight }) => {
-        const { databases } = this.state;
+    onChangeSegment = (item) => {
+        if (item.segment != null) {
+            this.props.setSourceSegmentFn && this.props.setSourceSegmentFn(item.segment.id);
+            this.nextStep({ selectedSegment: item.segment })
+        }
+    }
 
-        if (databases.length === 0) {
-            return <LoadingAndErrorWrapper loading />;
+    onShowSegmentSection = () => {
+        // Jumping to the next step SEGMENT_OR_TABLE_STEP without a db/schema
+        // indicates that we want to show the segment section
+        this.nextStep({ selectedDatabase: null, selectedSchema: null })
+    }
+
+    getTriggerElement() {
+        const { className, style, triggerIconSize, getTriggerElementContent } = this.props
+        const { selectedDatabase, selectedSegment, selectedTable, selectedField } = this.state;
+
+        return (
+            <span className={className || "px2 py2 text-bold cursor-pointer text-default"} style={style}>
+                { getTriggerElementContent({ selectedDatabase, selectedSegment, selectedTable, selectedField }) }
+                <Icon className="ml1" name="chevrondown" size={triggerIconSize || 8}/>
+            </span>
+        );
+    }
+
+    renderActiveStep() {
+        const { segments, skipDatabaseSelection, disabledTableIds, disabledSegmentIds } = this.props
+        const { databases, isLoading, selectedDatabase, selectedSchema, selectedTable, selectedField, selectedSegment } = this.state
+
+        const hasAdjacentStep = this.hasAdjacentStep()
+
+        switch(this.state.activeStep) {
+            case DATABASE_STEP: return <DatabasePicker
+                databases={databases}
+                selectedDatabase={selectedDatabase}
+                onChangeDatabase={this.onChangeDatabase}
+                hasAdjacentStep={hasAdjacentStep}
+            />;
+            case DATABASE_SCHEMA_STEP: return <DatabaseSchemaPicker
+                skipDatabaseSelection={skipDatabaseSelection}
+                databases={databases}
+                selectedDatabase={selectedDatabase}
+                selectedSchema={selectedSchema}
+                onChangeSchema={this.onChangeSchema}
+                onChangeDatabase={this.onChangeDatabase}
+                hasAdjacentStep={hasAdjacentStep}
+            />;
+            case SCHEMA_STEP: return <SchemaPicker
+                 selectedDatabase={selectedDatabase}
+                 selectedSchema={selectedSchema}
+                 onChangeSchema={this.onChangeSchema}
+                 hasAdjacentStep={hasAdjacentStep}
+            />;
+            case SCHEMA_AND_SEGMENTS_STEP: return <SegmentAndDatabasePicker
+                databases={databases}
+                selectedSchema={selectedSchema}
+                onChangeSchema={this.onChangeSchema}
+                onShowSegmentSection={this.onShowSegmentSection}
+                onChangeDatabase={this.onChangeDatabase}
+                hasAdjacentStep={hasAdjacentStep}
+            />;
+            case TABLE_STEP:
+                const canGoBack = this.hasPreviousStep()
+
+                return <TablePicker
+                     selectedDatabase={selectedDatabase}
+                     selectedSchema={selectedSchema}
+                     selectedTable={selectedTable}
+                     databases={databases}
+                     segments={segments}
+                     disabledTableIds={disabledTableIds}
+                     onChangeTable={this.onChangeTable}
+                     onBack={canGoBack && this.onBack}
+                     hasAdjacentStep={hasAdjacentStep}
+                />;
+            case FIELD_STEP: return <FieldPicker
+                     isLoading={isLoading}
+                     selectedTable={selectedTable}
+                     selectedField={selectedField}
+                     onChangeField={this.onChangeField}
+                     onBack={this.onBack}
+                />;
+            case SEGMENT_OR_TABLE_STEP:
+                if (selectedDatabase && selectedSchema) {
+                    return <TablePicker
+                         selectedDatabase={selectedDatabase}
+                         selectedSchema={selectedSchema}
+                         selectedTable={selectedTable}
+                         databases={databases}
+                         segments={segments}
+                         disabledTableIds={disabledTableIds}
+                         onChangeTable={this.onChangeTable}
+                         hasPreviousStep={this.hasPreviousStep}
+                         onBack={this.onBack}
+                    />
+                } else {
+                    return <SegmentPicker
+                        segments={segments}
+                        selectedSegment={selectedSegment}
+                        disabledSegmentIds={disabledSegmentIds}
+                        onBack={this.onBack}
+                        onChangeSegment={this.onChangeSegment}
+                    />
+                }
         }
 
-        let sections = [{
-            items: databases.map(database => ({
-                name: database.name,
-                database: database
-            }))
-        }];
+        return null;
+    }
 
+    render() {
+        const triggerClasses = this.props.renderAsSelect ? "border-med bg-white block no-decoration" : "flex align-center";
         return (
+            <PopoverWithTrigger
+                id="DataPopover"
+                ref="popover"
+                isInitiallyOpen={this.props.isInitiallyOpen}
+                triggerElement={this.getTriggerElement()}
+                triggerClasses={triggerClasses}
+                horizontalAttachments={["center", "left", "right"]}
+            >
+                { this.renderActiveStep() }
+            </PopoverWithTrigger>
+        );
+    }
+}
+
+const DatabasePicker = ({ databases, selectedDatabase, onChangeDatabase, hasAdjacentStep }) => {
+    if (databases.length === 0) {
+        return <DataSelectorLoading />
+    }
+
+    let sections = [{
+        items: databases.map((database, index) => ({
+            name: database.name,
+            index,
+            database: database
+        }))
+    }];
+
+    return (
+        <AccordianList
+            id="DatabasePicker"
+            key="databasePicker"
+            className="text-brand"
+            sections={sections}
+            onChange={(db) => onChangeDatabase(db.index)}
+            itemIsSelected={(item) => selectedDatabase && item.database.id === selectedDatabase.id}
+            renderItemIcon={() => <Icon className="Icon text-default" name="database" size={18} />}
+            showItemArrows={hasAdjacentStep}
+        />
+    );
+}
+
+const SegmentAndDatabasePicker = ({ databases, selectedSchema, onChangeSchema, onShowSegmentSection, onChangeDatabase, hasAdjacentStep }) => {
+    const segmentItem = [{ name: 'Segments', items: [], icon: 'segment'}];
+
+    const sections = segmentItem.concat(databases.map(database => {
+        return {
+            name: database.name,
+            items: database.schemas.length > 1 ? database.schemas : []
+        };
+    }));
+
+    // FIXME: this seems a bit brittle and hard to follow
+    let openSection = selectedSchema && (_.findIndex(databases, (db) => _.find(db.schemas, selectedSchema)) + segmentItem.length);
+    if (openSection >= 0 && databases[openSection - segmentItem.length] && databases[openSection - segmentItem.length].schemas.length === 1) {
+        openSection = -1;
+    }
+
+    return (
+        <AccordianList
+            id="SegmentAndDatabasePicker"
+            key="segmentAndDatabasePicker"
+            className="text-brand"
+            sections={sections}
+            onChange={onChangeSchema}
+            onChangeSection={(index) => {
+                index === 0
+                    ? onShowSegmentSection()
+                    : onChangeDatabase(index - segmentItem.length, true)
+            }}
+            itemIsSelected={(schema) => selectedSchema === schema}
+            renderSectionIcon={(section) => <Icon className="Icon text-default" name={section.icon || "database"} size={18} />}
+            renderItemIcon={() => <Icon name="folder" size={16} />}
+            initiallyOpenSection={openSection}
+            showItemArrows={hasAdjacentStep}
+            alwaysTogglable={true}
+        />
+    );
+}
+
+export const SchemaPicker = ({ selectedDatabase, selectedSchema, onChangeSchema, hasAdjacentStep }) => {
+    let sections = [{
+        items: selectedDatabase.schemas
+    }];
+    return (
+        <div style={{ width: 300 }}>
             <AccordianList
-                id="DatabasePicker"
-                key="databasePicker"
+                id="DatabaseSchemaPicker"
+                key="databaseSchemaPicker"
                 className="text-brand"
-                maxHeight={maxHeight}
                 sections={sections}
-                onChange={this.onChangeTable}
-                itemIsSelected={(item) => this.getDatabaseId() === item.database.id}
-                renderItemIcon={() => <Icon className="Icon text-default" name="database" size={18} />}
-                showItemArrows={false}
+                searchable
+                onChange={onChangeSchema}
+                itemIsSelected={(schema) => schema === selectedSchema}
+                renderItemIcon={() => <Icon name="folder" size={16} />}
+                showItemArrows={hasAdjacentStep}
             />
-        );
-    }
-
-    renderDatabaseSchemaPicker = ({ maxHeight }) => {
-        const { databases, selectedSchema } = this.state;
+        </div>
+    );
+}
 
+export const DatabaseSchemaPicker = ({ skipDatabaseSelection, databases, selectedDatabase, selectedSchema, onChangeSchema, onChangeDatabase, hasAdjacentStep }) => {
         if (databases.length === 0) {
-            return <LoadingAndErrorWrapper loading />;
+            return <DataSelectorLoading />
         }
 
-        let sections = databases
-            .map(database => ({
-                name: database.name,
-                items: database.schemas.length > 1 ? database.schemas : [],
-                className: database.is_saved_questions ? "bg-slate-extra-light" : null,
-                icon: database.is_saved_questions ? 'all' : 'database'
-            }));
+        const sections = databases.map(database => ({
+            name: database.name,
+            items: database.schemas.length > 1 ? database.schemas : [],
+            className: database.is_saved_questions ? "bg-slate-extra-light" : null,
+            icon: database.is_saved_questions ? 'all' : 'database'
+        }));
 
         let openSection = selectedSchema && _.findIndex(databases, (db) => _.find(db.schemas, selectedSchema));
         if (openSection >= 0 && databases[openSection] && databases[openSection].schemas.length === 1) {
@@ -202,11 +538,10 @@ export default class DataSelector extends Component {
                     id="DatabaseSchemaPicker"
                     key="databaseSchemaPicker"
                     className="text-brand"
-                    maxHeight={maxHeight}
                     sections={sections}
-                    onChange={this.onChangeSchema}
-                    onChangeSection={this.onChangeDatabase}
-                    itemIsSelected={(schema) => this.state.selectedSchema === schema}
+                    onChange={onChangeSchema}
+                    onChangeSection={(dbId) => onChangeDatabase(dbId, true)}
+                    itemIsSelected={(schema) => schema === selectedSchema}
                     renderSectionIcon={item =>
                         <Icon
                             className="Icon text-default"
@@ -216,234 +551,184 @@ export default class DataSelector extends Component {
                     }
                     renderItemIcon={() => <Icon name="folder" size={16} />}
                     initiallyOpenSection={openSection}
-                    showItemArrows={true}
                     alwaysTogglable={true}
+                    showItemArrows={hasAdjacentStep}
                 />
             </div>
         );
-    }
 
-    renderSegmentAndDatabasePicker = ({ maxHeight }) => {
-        const { selectedSchema } = this.state;
-
-        const segmentItem = [{ name: 'Segments', items: [], icon: 'segment'}];
-
-        const sections = segmentItem.concat(this.state.databases.map(database => {
-            return {
-                name: database.name,
-                items: database.schemas.length > 1 ? database.schemas : []
-            };
-        }));
-
-        // FIXME: this seems a bit brittle and hard to follow
-        let openSection = selectedSchema && (_.findIndex(this.state.databases, (db) => _.find(db.schemas, selectedSchema)) + segmentItem.length);
-        if (openSection >= 0 && this.state.databases[openSection - segmentItem.length] && this.state.databases[openSection - segmentItem.length].schemas.length === 1) {
-            openSection = -1;
-        }
-
-        return (
-            <AccordianList
-                id="SegmentAndDatabasePicker"
-                key="segmentAndDatabasePicker"
-                className="text-brand"
-                maxHeight={maxHeight}
-                sections={sections}
-                onChange={this.onChangeSchema}
-                onChangeSection={(index) => index === 0 ?
-                    this.onChangeSegmentSection() :
-                    this.onChangeDatabase(index - segmentItem.length)
-                }
-                itemIsSelected={(schema) => this.state.selectedSchema === schema}
-                renderSectionIcon={(section, sectionIndex) => <Icon className="Icon text-default" name={section.icon || "database"} size={18} />}
-                renderItemIcon={() => <Icon name="folder" size={16} />}
-                initiallyOpenSection={openSection}
-                showItemArrows={true}
-                alwaysTogglable={true}
-            />
-        );
     }
 
-    renderTablePicker = ({ maxHeight }) => {
-        const schema = this.state.selectedSchema;
-
-        const isSavedQuestionList = schema.database.is_saved_questions;
-
-        const hasMultipleDatabases = this.props.databases.length > 1;
-        const hasMultipleSchemas = schema && schema.database && _.uniq(schema.database.tables, (t) => t.schema).length > 1;
-        const hasSegments = !!this.props.segments;
-        const hasMultipleSources = hasMultipleDatabases || hasMultipleSchemas || hasSegments;
-
-        let header = (
-            <div className="flex flex-wrap align-center">
-                <span className="flex align-center text-brand-hover cursor-pointer" onClick={hasMultipleSources && this.onBack}>
-                    {hasMultipleSources && <Icon name="chevronleft" size={18} /> }
-                    <span className="ml1">{schema.database.name}</span>
+export const TablePicker = ({ selectedDatabase, selectedSchema, selectedTable, disabledTableIds, onChangeTable, hasAdjacentStep, onBack }) => {
+    const isSavedQuestionList = selectedDatabase.is_saved_questions;
+    let header = (
+        <div className="flex flex-wrap align-center">
+                <span className={cx("flex align-center", { "text-brand-hover cursor-pointer": onBack })} onClick={onBack}>
+                    {onBack && <Icon name="chevronleft" size={18} /> }
+                    <span className="ml1">{selectedDatabase.name}</span>
                 </span>
-                { schema.name && <span className="ml1 text-slate">- {schema.name}</span>}
-            </div>
-        );
+            { selectedSchema.name && <span className="ml1 text-slate">- {selectedSchema.name}</span>}
+        </div>
+    );
 
-        if (schema.tables.length === 0) {
-            // this is a database with no tables!
-            return (
-                <section className="List-section List-section--open" style={{width: 300}}>
-                    <div className="p1 border-bottom">
-                        <div className="px1 py1 flex align-center">
-                            <h3 className="text-default">{header}</h3>
-                        </div>
+    if (selectedSchema.tables.length === 0) {
+        // this is a database with no tables!
+        return (
+            <section className="List-section List-section--open" style={{width: 300}}>
+                <div className="p1 border-bottom">
+                    <div className="px1 py1 flex align-center">
+                        <h3 className="text-default">{header}</h3>
                     </div>
-                    <div className="p4 text-centered">{t`No tables found in this database.`}</div>
-                </section>
-            );
-        } else {
-            let sections = [{
-                name: header,
-                items: schema.tables
-                    .map(table => ({
-                        name: table.display_name,
-                        disabled: this.props.disabledTableIds && this.props.disabledTableIds.includes(table.id),
-                        table: table,
-                        database: schema.database
-                    }))
-            }];
-            return (
-                <div style={{ width: 300 }}>
-                    <AccordianList
-                        id="TablePicker"
-                        key="tablePicker"
-                        className="text-brand"
-                        maxHeight={maxHeight}
-                        sections={sections}
-                        searchable
-                        onChange={this.onChangeTable}
-                        itemIsSelected={(item) => item.table ? item.table.id === this.getTableId() : false}
-                        itemIsClickable={(item) => item.table && !item.disabled}
-                        renderItemIcon={(item) => item.table ? <Icon name="table2" size={18} /> : null}
-                    />
-                    { isSavedQuestionList && (
-                        <div className="bg-slate-extra-light p2 text-centered border-top">
-                            {t`Is a question missing?`}
-                            <a href="http://metabase.com/docs/latest/users-guide/04-asking-questions.html#source-data" className="block link">{t`Learn more about nested queries`}</a>
-                        </div>
-                    )}
                 </div>
-            );
-        }
+                <div className="p4 text-centered">{t`No tables found in this database.`}</div>
+            </section>
+        );
+    } else {
+        let sections = [{
+            name: header,
+            items: selectedSchema.tables
+                .map(table => ({
+                    name: table.display_name,
+                    disabled: disabledTableIds && disabledTableIds.includes(table.id),
+                    table: table,
+                    database: selectedDatabase
+                }))
+        }];
+        return (
+            <div style={{ width: 300 }}>
+                <AccordianList
+                    id="TablePicker"
+                    key="tablePicker"
+                    className="text-brand"
+                    sections={sections}
+                    searchable
+                    onChange={onChangeTable}
+                    itemIsSelected={(item) => (item.table && selectedTable) ? item.table.id === selectedTable.id : false}
+                    itemIsClickable={(item) => item.table && !item.disabled}
+                    renderItemIcon={(item) => item.table ? <Icon name="table2" size={18} /> : null}
+                    showItemArrows={hasAdjacentStep}
+                />
+                { isSavedQuestionList && (
+                    <div className="bg-slate-extra-light p2 text-centered border-top">
+                        {t`Is a question missing?`}
+                        <a href="http://metabase.com/docs/latest/users-guide/04-asking-questions.html#source-data" className="block link">{t`Learn more about nested queries`}</a>
+                    </div>
+                )}
+            </div>
+        );
     }
+}
+
+@connect(state => ({metadata: getMetadata(state)}))
+export class FieldPicker extends Component {
+    render() {
+        const { isLoading, selectedTable, selectedField, onChangeField, metadata, onBack } = this.props
 
-    //TODO: refactor this. lots of shared code with renderTablePicker = () =>
-    renderSegmentPicker = ({ maxHeight }) => {
-        const { segments } = this.props;
         const header = (
             <span className="flex align-center">
-                <span className="flex align-center text-slate cursor-pointer" onClick={this.onBack}>
-                    <Icon name="chevronleft" size={18} />
-                    <span className="ml1">{t`Segments`}</span>
+                    <span className="flex align-center text-slate cursor-pointer" onClick={onBack}>
+                        <Icon name="chevronleft" size={18} />
+                        <span className="ml1">{ selectedTable.display_name || t`Fields`}</span>
+                    </span>
                 </span>
-            </span>
         );
 
-        if (!segments || segments.length === 0) {
-            return (
-                <section className="List-section List-section--open" style={{width: '300px'}}>
-                    <div className="p1 border-bottom">
-                        <div className="px1 py1 flex align-center">
-                            <h3 className="text-default">{header}</h3>
-                        </div>
-                    </div>
-                    <div className="p4 text-centered">{t`No segments were found.`}</div>
-                </section>
-            );
+        if (isLoading) {
+            return <DataSelectorLoading header={header} />
         }
 
+        const table = metadata.tables[selectedTable.id];
+        const fields = (table && table.fields) || [];
         const sections = [{
             name: header,
-            items: segments
-                .map(segment => ({
-                    name: segment.name,
-                    segment: segment,
-                    disabled: this.props.disabledSegmentIds && this.props.disabledSegmentIds.includes(segment.id)
-                }))
+            items: fields.map(field => ({
+                name: field.display_name,
+                field: field,
+            }))
         }];
 
         return (
-            <AccordianList
-                id="SegmentPicker"
-                key="segmentPicker"
-                className="text-brand"
-                maxHeight={maxHeight}
-                sections={sections}
-                searchable
-                searchPlaceholder={t`Find a segment`}
-                onChange={this.onChangeSegment}
-                itemIsSelected={(item) => item.segment ? item.segment.id === this.getSegmentId() : false}
-                itemIsClickable={(item) => item.segment && !item.disabled}
-                renderItemIcon={(item) => item.segment ? <Icon name="segment" size={18} /> : null}
-                hideSingleSectionTitle={true}
-            />
+            <div style={{ width: 300 }}>
+                <AccordianList
+                    id="FieldPicker"
+                    key="fieldPicker"
+                    className="text-brand"
+                    sections={sections}
+                    searchable
+                    onChange={onChangeField}
+                    itemIsSelected={(item) => (item.field && selectedField) ? (item.field.id === selectedField.id) : false}
+                    itemIsClickable={(item) => item.field && !item.disabled}
+                    renderItemIcon={(item) => item.field ? <Icon name={item.field.dimension().icon()} size={18} /> : null}
+                />
+            </div>
         );
     }
+}
 
-    render() {
-        const { databases } = this.props;
-
-        let dbId = this.getDatabaseId();
-        let tableId = this.getTableId();
-        var database = _.find(databases, (db) => db.id === dbId);
-        var table = _.find(database && database.tables, (table) => table.id === tableId);
-
-        var content;
-        if (this.props.includeTables && this.props.segments) {
-            const segmentId = this.getSegmentId();
-            const segment = _.find(this.props.segments, (segment) => segment.id === segmentId);
-            if (table) {
-                content = <span className="text-grey no-decoration">{table.display_name || table.name}</span>;
-            } else if (segment) {
-                content = <span className="text-grey no-decoration">{segment.name}</span>;
-            } else {
-                content = <span className="text-grey-4 no-decoration">{t`Pick a segment or table`}</span>;
-            }
-        } else if (this.props.includeTables) {
-            if (table) {
-                content = <span className="text-grey no-decoration">{table.display_name || table.name}</span>;
-            } else {
-                content = <span className="text-grey-4 no-decoration">{t`Select a table`}</span>;
-            }
-        } else {
-            if (database) {
-                content = <span className="text-grey no-decoration">{database.name}</span>;
-            } else {
-                content = <span className="text-grey-4 no-decoration">{t`Select a database`}</span>;
-            }
-        }
-
-        var triggerElement = (
-            <span className={this.props.className || "px2 py2 text-bold cursor-pointer text-default"} style={this.props.style}>
-                {content}
-                <Icon className="ml1" name="chevrondown" size={this.props.triggerIconSize || 8}/>
+//TODO: refactor this. lots of shared code with renderTablePicker = () =>
+export const SegmentPicker = ({ segments, selectedSegment, disabledSegmentIds, onBack, onChangeSegment }) => {
+    const header = (
+        <span className="flex align-center">
+                <span className="flex align-center text-slate cursor-pointer" onClick={onBack}>
+                    <Icon name="chevronleft" size={18} />
+                    <span className="ml1">{t`Segments`}</span>
+                </span>
             </span>
-        )
+    );
 
+    if (!segments || segments.length === 0) {
         return (
-            <PopoverWithTrigger
-                id="DataPopover"
-                ref="popover"
-                isInitiallyOpen={this.props.isInitiallyOpen}
-                triggerElement={triggerElement}
-                triggerClasses="flex align-center"
-                horizontalAttachments={this.props.segments ? ["center", "left", "right"] : ["left"]}
-            >
-                { !this.props.includeTables ?
-                    this.renderDatabasePicker :
-                    this.state.selectedSchema && this.state.showTablePicker ?
-                        this.renderTablePicker :
-                        this.props.segments ?
-                            this.state.showSegmentPicker ?
-                                this.renderSegmentPicker :
-                                this.renderSegmentAndDatabasePicker :
-                            this.renderDatabaseSchemaPicker
-                }
-            </PopoverWithTrigger>
+            <section className="List-section List-section--open" style={{width: '300px'}}>
+                <div className="p1 border-bottom">
+                    <div className="px1 py1 flex align-center">
+                        <h3 className="text-default">{header}</h3>
+                    </div>
+                </div>
+                <div className="p4 text-centered">{t`No segments were found.`}</div>
+            </section>
+        );
+    }
+
+    const sections = [{
+        name: header,
+        items: segments
+            .map(segment => ({
+                name: segment.name,
+                segment: segment,
+                disabled: disabledSegmentIds && disabledSegmentIds.includes(segment.id)
+            }))
+    }];
+
+    return (
+        <AccordianList
+            id="SegmentPicker"
+            key="segmentPicker"
+            className="text-brand"
+            sections={sections}
+            searchable
+            searchPlaceholder={t`Find a segment`}
+            onChange={onChangeSegment}
+            itemIsSelected={(item) => selectedSegment && item.segment ? item.segment.id === selectedSegment.id : false}
+            itemIsClickable={(item) => item.segment && !item.disabled}
+            renderItemIcon={(item) => item.segment ? <Icon name="segment" size={18} /> : null}
+        />
+    );
+}
+
+const DataSelectorLoading = ({ header }) => {
+    if (header) {
+        return (
+            <section className="List-section List-section--open" style={{width: 300}}>
+                <div className="p1 border-bottom">
+                    <div className="px1 py1 flex align-center">
+                        <h3 className="text-default">{header}</h3>
+                    </div>
+                </div>
+                <LoadingAndErrorWrapper loading />;
+            </section>
         );
+    } else {
+        return <LoadingAndErrorWrapper loading />;
     }
 }
+
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index 1b8b134f0d8d9725cf5d2df8c4ee2ca8af37cd07..cc03fd2c85f8a4e3fa4d132ca3a663b34337779d 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -6,13 +6,13 @@ import ReactDOM from "react-dom";
 import { t } from 'c-3po';
 import AggregationWidget_LEGACY from './AggregationWidget.jsx';
 import BreakoutWidget_LEGACY from './BreakoutWidget.jsx';
-import DataSelector from './DataSelector.jsx';
 import ExtendedOptions from "./ExtendedOptions.jsx";
 import FilterList from './filters/FilterList.jsx';
 import FilterPopover from './filters/FilterPopover.jsx';
 import Icon from "metabase/components/Icon.jsx";
 import IconBorder from 'metabase/components/IconBorder.jsx';
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
+import { DatabaseSchemaAndTableDataSelector } from "metabase/query_builder/components/DataSelector";
 
 import cx from "classnames";
 import _ from "underscore";
@@ -274,21 +274,23 @@ export default class GuiQueryEditor extends Component {
     }
 
     renderDataSection() {
-        const { query } = this.props;
+        const { databases, query, isShowingTutorial } = this.props;
         const tableMetadata = query.tableMetadata();
+        const datasetQuery = query.datasetQuery();
+        const sourceTableId = datasetQuery && datasetQuery.query && datasetQuery.query.source_table;
+        const isInitiallyOpen = (!datasetQuery.database || !sourceTableId) && !isShowingTutorial;
+
         return (
             <div className={"GuiBuilder-section GuiBuilder-data flex align-center arrow-right"}>
                 <span className="GuiBuilder-section-label Query-label">{t`Data`}</span>
                 { this.props.features.data ?
-                    <DataSelector
+                    <DatabaseSchemaAndTableDataSelector
                         ref="dataSection"
-                        includeTables={true}
-                        datasetQuery={query.datasetQuery()}
-                        databases={this.props.databases}
-                        tables={this.props.tables}
+                        databases={databases}
+                        selectedTableId={sourceTableId}
                         setDatabaseFn={this.props.setDatabaseFn}
                         setSourceTableFn={this.props.setSourceTableFn}
-                        isInitiallyOpen={(!query.datasetQuery().database || !query.query().source_table) && !this.props.isShowingTutorial}
+                        isInitiallyOpen={isInitiallyOpen}
                     />
                     :
                     <span className="flex align-center px2 py2 text-bold text-grey">
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index 16a8e653f2887a6d45e05f75f62c2590c61c0caa..0a457eb9b7267b900c4a0b6492eba939cbb8ff22 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -29,7 +29,6 @@ import { SQLBehaviour } from "metabase/lib/ace/sql_behaviour";
 
 import _ from "underscore";
 
-import DataSelector from './DataSelector.jsx';
 import Icon from "metabase/components/Icon.jsx";
 import Parameters from "metabase/parameters/components/Parameters";
 
@@ -50,6 +49,10 @@ import type { TableId } from "metabase/meta/types/Table";
 import type { ParameterId } from "metabase/meta/types/Parameter";
 import type { LocationDescriptor } from "metabase/meta/types";
 import type { RunQueryParams } from "metabase/query_builder/actions";
+import {
+    DatabaseDataSelector,
+    SchemaAndTableDataSelector,
+} from "metabase/query_builder/components/DataSelector";
 
 type AutoCompleteResult = [string, string, string];
 type AceEditor = any; // TODO;
@@ -268,9 +271,9 @@ export default class NativeQueryEditor extends Component {
                 dataSelectors.push(
                     <div key="db_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
                         <span className="GuiBuilder-section-label Query-label">{t`Database`}</span>
-                        <DataSelector
+                        <DatabaseDataSelector
                             databases={databases}
-                            datasetQuery={query.datasetQuery()}
+                            selectedDatabaseId={database && database.id}
                             setDatabaseFn={this.setDatabaseId}
                             isInitiallyOpen={database == null}
                         />
@@ -288,17 +291,12 @@ export default class NativeQueryEditor extends Component {
                 dataSelectors.push(
                     <div key="table_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
                         <span className="GuiBuilder-section-label Query-label">{t`Table`}</span>
-                        <DataSelector
+                        <SchemaAndTableDataSelector
                             ref="dataSection"
-                            includeTables={true}
-                            datasetQuery={{
-                                type: "query",
-                                query: { source_table: selectedTable ? selectedTable.id : null },
-                                database: database && database.id
-                            }}
+                            selectedTableId={selectedTable ? selectedTable.id : null}
+                            selectedDatabaseId={database && database.id}
                             databases={[database]}
                             tables={tables}
-                            setDatabaseFn={this.setDatabaseId}
                             setSourceTableFn={this.setTableId}
                             isInitiallyOpen={false}
                         />
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
index 4c75c96f2d47c74c423bc020078cc10c5430218f..589838e5a7a06582f18b06b5d0525f514377000a 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
@@ -7,6 +7,7 @@ import FieldList from "../FieldList.jsx";
 import OperatorSelector from "./OperatorSelector.jsx";
 import FilterOptions from "./FilterOptions";
 import DatePicker from "./pickers/DatePicker.jsx";
+import TimePicker from "./pickers/TimePicker.jsx";
 import NumberPicker from "./pickers/NumberPicker.jsx";
 import SelectPicker from "./pickers/SelectPicker.jsx";
 import TextPicker from "./pickers/TextPicker.jsx";
@@ -14,7 +15,7 @@ import TextPicker from "./pickers/TextPicker.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
 import Query from "metabase/lib/query";
-import { isDate } from "metabase/lib/schema_metadata";
+import { isDate, isTime } from "metabase/lib/schema_metadata";
 import { formatField, singularize } from "metabase/lib/formatting";
 
 import cx from "classnames";
@@ -276,7 +277,13 @@ export default class FilterPopover extends Component {
                         <h3 className="mx1">-</h3>
                         <h3 className="text-default">{formatField(field)}</h3>
                     </div>
-                    { isDate(field) ?
+                    { isTime(field) ?
+                        <TimePicker
+                            className="mt1 border-top"
+                            filter={filter}
+                            onFilterChange={this.setFilter}
+                        />
+                    : isDate(field) ?
                         <DatePicker
                             className="mt1 border-top"
                             filter={filter}
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
index 74aea4289252ee59941ca031a094443ce8516036..b61a60123e4df9aefc290a57ab4e47d6b0b0fda9 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
@@ -67,7 +67,7 @@ export default class FilterWidget extends Component {
         // $FlowFixMe: not understanding maxDisplayValues is provided by defaultProps
         if (operator && operator.multi && values.length > maxDisplayValues) {
             formattedValues = [values.length + " selections"];
-        } else if (dimension.field().isDate()) {
+        } else if (dimension.field().isDate() && !dimension.field().isTime()) {
             formattedValues = generateTimeFilterValuesDescriptions(filter);
         } else {
             // TODO Atte Keinänen 7/16/17: Move formatValue to metabase-lib
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
index 55e367956a3cf68d075e4b9c2391dd0ef4dbc1e7..ee916150524267e31fbb079dde1b435e4e57854a 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
@@ -1,6 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
+import PropTypes from "prop-types";
 import { t } from 'c-3po';
 import cx from 'classnames';
 import moment from "moment";
@@ -65,9 +66,13 @@ const MultiDatePicker = ({ filter: [op, field, startValue, endValue], onFilterCh
 const PreviousPicker =  (props) =>
     <RelativeDatePicker {...props} formatter={(value) => value * -1} />
 
+PreviousPicker.horizontalLayout = true;
+
 const NextPicker = (props) =>
     <RelativeDatePicker {...props} />
 
+NextPicker.horizontalLayout = true;
+
 type CurrentPickerProps = {
     filter: TimeIntervalFilter,
     onFilterChange: (filter: TimeIntervalFilter) => void
@@ -85,6 +90,8 @@ class CurrentPicker extends Component {
         showUnits: false
     };
 
+    static horizontalLayout = true;
+
     render() {
         const { filter: [operator, field, intervals, unit], onFilterChange } = this.props
         return (
@@ -105,7 +112,6 @@ class CurrentPicker extends Component {
     }
 }
 
-
 const getIntervals = ([op, field, value, unit]) => mbqlEq(op, "time-interval") && typeof value === "number" ? Math.abs(value) : 30;
 const getUnit      = ([op, field, value, unit]) => mbqlEq(op, "time-interval") && unit ? unit : "day";
 const getOptions   = ([op, field, value, unit, options]) => mbqlEq(op, "time-interval") && options || {};
@@ -129,7 +135,7 @@ function getDateTimeField(field: ConcreteField, bucketing: ?DatetimeUnit): Concr
     }
 }
 
-function getDateTimeFieldTarget(field: ConcreteField): LocalFieldReference|ForeignFieldReference|ExpressionReference {
+export function getDateTimeFieldTarget(field: ConcreteField): LocalFieldReference|ForeignFieldReference|ExpressionReference {
     if (Query.isDatetimeField(field)) {
         // $FlowFixMe:
         return (field[1]: LocalFieldReference|ForeignFieldReference|ExpressionReference);
@@ -221,7 +227,6 @@ export const DATE_OPERATORS: Operator[] = [
         test: ([op]) => mbqlEq(op, "between"),
         widget: MultiDatePicker,
     },
-
 ];
 
 export const EMPTINESS_OPERATORS: Operator[] = [
@@ -252,6 +257,7 @@ type Props = {
     hideEmptinessOperators?: boolean, // Don't show is empty / not empty dialog
     hideTimeSelectors?: boolean,
     includeAllTime?: boolean,
+    operators?: Operator[],
 }
 
 type State = {
@@ -264,8 +270,20 @@ export default class DatePicker extends Component {
         operators: []
     };
 
+    static propTypes = {
+        filter: PropTypes.array.isRequired,
+        onFilterChange: PropTypes.func.isRequired,
+        className: PropTypes.string,
+        hideEmptinessOperators: PropTypes.bool,
+        hideTimeSelectors: PropTypes.bool,
+        operators: PropTypes.array,
+    };
+
     componentWillMount() {
-        const operators = this.props.hideEmptinessOperators ? DATE_OPERATORS : ALL_OPERATORS;
+        let operators = this.props.operators || DATE_OPERATORS;
+        if (!this.props.hideEmptinessOperators) {
+            operators = operators.concat(EMPTINESS_OPERATORS);
+        }
 
         const operator = getOperator(this.props.filter, operators) || operators[0];
         this.props.onFilterChange(operator.init(this.props.filter));
@@ -283,19 +301,10 @@ export default class DatePicker extends Component {
         const operator = getOperator(this.props.filter, operators);
         const Widget = operator && operator.widget;
 
-        // certain types of operators need to have a horizontal layout
-        // where the value is chosen next to the operator selector
-        // TODO - there's no doubt a cleaner _ way to do this
-        const needsHorizontalLayout = operator && (
-            operator.name === "current"  ||
-            operator.name === "previous" ||
-            operator.name === "next"
-        );
-
         return (
             <div
               // apply flex to align the operator selector and the "Widget" if necessary
-              className={cx("border-top pt2", { "flex align-center": needsHorizontalLayout })}
+              className={cx("border-top pt2", { "flex align-center": Widget && Widget.horizontalLayout })}
               style={{ minWidth: 380 }}
             >
                 <DateOperatorSelector
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..0472b54747a78cf720c826262b2ec7e96f234030
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx
@@ -0,0 +1,40 @@
+import React from "react";
+
+import NumericInput from "./NumericInput";
+import Icon from "metabase/components/Icon";
+
+import cx from "classnames";
+
+const HoursMinutesInput = ({ hours, minutes, onChangeHours, onChangeMinutes, onClear }) =>
+    <div className="flex align-center">
+        <NumericInput
+            className="input"
+            style={{ height: 36 }}
+            size={2}
+            maxLength={2}
+            value={(hours % 12) === 0 ? "12" : String(hours % 12)}
+            onChange={(value) => onChangeHours((hours >= 12 ? 12 : 0) + value) }
+        />
+        <span className="px1">:</span>
+        <NumericInput
+            className="input"
+            style={{ height: 36 }}
+            size={2}
+            maxLength={2}
+            value={(minutes < 10 ? "0" : "") + minutes}
+            onChange={(value) => onChangeMinutes(value) }
+        />
+        <div className="flex align-center pl1">
+            <span className={cx("text-purple-hover mr1", { "text-purple": hours < 12, "cursor-pointer": hours >= 12 })} onClick={hours >= 12 ? () => onChangeHours(hours - 12) : null}>AM</span>
+            <span className={cx("text-purple-hover mr1", { "text-purple": hours >= 12, "cursor-pointer": hours < 12 })} onClick={hours < 12 ? () => onChangeHours(hours + 12) : null}>PM</span>
+        </div>
+        { onClear &&
+            <Icon
+                className="text-grey-2 cursor-pointer text-grey-4-hover ml-auto"
+                name="close"
+                onClick={onClear}
+            />
+        }
+    </div>
+
+export default HoursMinutesInput;
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
index dbca125f17d187e477b8759e562b213c9e763d4f..b9e677b064405a27357e800eb33dff8772b48a6a 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
@@ -8,7 +8,7 @@ import Input from "metabase/components/Input";
 import Icon from "metabase/components/Icon";
 import ExpandingContent from "metabase/components/ExpandingContent";
 import Tooltip from "metabase/components/Tooltip";
-import NumericInput from "./NumericInput.jsx";
+import HoursMinutesInput from "./HoursMinutesInput";
 
 import moment from "moment";
 import cx from "classnames";
@@ -146,8 +146,8 @@ export default class SpecificDatePicker extends Component {
                                 Add a time
                             </div>
                             :
-                            <HoursMinutes
-                                clear={() => this.onChange(date, null, null)}
+                            <HoursMinutesInput
+                                onClear={() => this.onChange(date, null, null)}
                                 hours={hours}
                                 minutes={minutes}
                                 onChangeHours={hours => this.onChange(date, hours, minutes)}
@@ -160,31 +160,3 @@ export default class SpecificDatePicker extends Component {
         )
     }
 }
-
-const HoursMinutes = ({ hours, minutes, onChangeHours, onChangeMinutes, clear }) =>
-    <div className="flex align-center">
-        <NumericInput
-            className="input"
-            size={2}
-            maxLength={2}
-            value={(hours % 12) === 0 ? "12" : String(hours % 12)}
-            onChange={(value) => onChangeHours((hours >= 12 ? 12 : 0) + value) }
-        />
-        <span className="px1">:</span>
-        <NumericInput
-            className="input"
-            size={2}
-            maxLength={2}
-            value={minutes}
-            onChange={(value) => onChangeMinutes(value) }
-        />
-        <div className="flex align-center pl1">
-            <span className={cx("text-purple-hover mr1", { "text-purple": hours < 12, "cursor-pointer": hours >= 12 })} onClick={hours >= 12 ? () => onChangeHours(hours - 12) : null}>AM</span>
-            <span className={cx("text-purple-hover mr1", { "text-purple": hours >= 12, "cursor-pointer": hours < 12 })} onClick={hours < 12 ? () => onChangeHours(hours + 12) : null}>PM</span>
-        </div>
-        <Icon
-            className="text-grey-2 cursor-pointer text-grey-4-hover ml-auto"
-            name="close"
-            onClick={() => clear() }
-        />
-    </div>
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f8ee5c0719db4be0933858f4e138038b68c9da63
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx
@@ -0,0 +1,75 @@
+import React from "react";
+import { t } from 'c-3po';
+
+import DatePicker, { getDateTimeFieldTarget } from "./DatePicker";
+import HoursMinutesInput from "./HoursMinutesInput";
+import { mbqlEq } from "metabase/lib/query/util";
+import { parseTime } from "metabase/lib/time";
+
+const TimeInput = ({ value, onChange }) => {
+    const time = parseTime(value);
+    return (
+      <HoursMinutesInput
+        hours={time.hour()}
+        minutes={time.minute()}
+        onChangeHours={(hours) => onChange(time.hour(hours).format("HH:mm:00.000"))}
+        onChangeMinutes={(minutes) => onChange(time.minute(minutes).format("HH:mm:00.000"))}
+      />
+    );
+}
+
+const SingleTimePicker = ({ filter, onFilterChange }) =>
+  <div className="mx2 mb2">
+    <TimeInput value={getTime(filter[2])} onChange={(time) => onFilterChange([filter[0], filter[1], time])} />
+  </div>
+
+SingleTimePicker.horizontalLayout = true;
+
+const MultiTimePicker = ({ filter, onFilterChange }) =>
+  <div className="flex align-center justify-between mx2 mb1" style={{ minWidth: 480 }}>
+    <TimeInput value={getTime(filter[2])} onChange={(time) => onFilterChange([filter[0], filter[1], ...sortTimes(time, filter[3])])} />
+    <span className="h3">and</span>
+    <TimeInput value={getTime(filter[3])} onChange={(time) => onFilterChange([filter[0], filter[1], ...sortTimes(filter[2], time)])} />
+  </div>
+
+const sortTimes = (a, b) => {
+    console.log(parseTime(a).isAfter(parseTime(b)))
+    return parseTime(a).isAfter(parseTime(b)) ? [b, a] : [a, b];
+}
+
+const getTime = (value) => {
+    if (typeof value === "string" && /^\d+:\d+(:\d+(.\d+(\+\d+:\d+)?)?)?$/.test(value)) {
+      return value;
+    } else {
+      return "00:00:00.000+00:00"
+    }
+}
+
+export const TIME_OPERATORS: Operator[] = [
+  {
+      name: "before",
+      displayName: t`Before`,
+      init: (filter) =>  ["<", getDateTimeFieldTarget(filter[1]), getTime(filter[2])],
+      test: ([op]) => op === "<",
+      widget: SingleTimePicker,
+  },
+  {
+      name: "after",
+      displayName: t`After`,
+      init: (filter) => [">", getDateTimeFieldTarget(filter[1]), getTime(filter[2])],
+      test: ([op]) => op === ">",
+      widget: SingleTimePicker,
+  },
+  {
+      name: "between",
+      displayName: t`Between`,
+      init: (filter) => ["BETWEEN", getDateTimeFieldTarget(filter[1]), getTime(filter[2]), getTime(filter[3])],
+      test: ([op]) => mbqlEq(op, "between"),
+      widget: MultiTimePicker,
+  },
+]
+
+const TimePicker = (props) =>
+  <DatePicker {...props} operators={TIME_OPERATORS} />
+
+export default TimePicker;
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
index dcf16ebc92ea7eda9506c4f9a3c60683bbd934c9..002a479d5fcadc7693c6a64138274ce36617a3bd 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -1,29 +1,48 @@
-/* @flow weak */
-
 import React, { Component } from "react";
 import { t } from 'c-3po';
+import _ from "underscore";
+import { connect } from "react-redux";
+
 import Toggle from "metabase/components/Toggle.jsx";
 import Input from "metabase/components/Input.jsx";
 import Select, { Option } from "metabase/components/Select.jsx";
 import ParameterValueWidget from "metabase/parameters/components/ParameterValueWidget.jsx";
 
 import { parameterOptionsForField } from "metabase/meta/Dashboard";
-
-import _ from "underscore";
-
-import type { TemplateTag } from "metabase/meta/types/Query"
+import type { TemplateTag } from "metabase/meta/types/Query";
+import type { Database } from "metabase/meta/types/Database"
 
 import Field from "metabase-lib/lib/metadata/Field";
+import { fetchField } from "metabase/redux/metadata";
+import { getMetadata } from "metabase/selectors/metadata";
+import { SchemaTableAndFieldDataSelector } from "metabase/query_builder/components/DataSelector";
+import Metadata from "metabase-lib/lib/metadata/Metadata";
+import type { FieldId } from "metabase/meta/types/Field";
 
 type Props = {
     tag: TemplateTag,
     onUpdate: (tag: TemplateTag) => void,
-    databaseFields: Field[]
-}
-
+    databaseFields: Field[],
+    database: Database,
+    databases: Database[],
+    metadata: Metadata,
+    fetchField: (FieldId) => void
+};
+
+@connect((state) => ({ metadata: getMetadata(state) }),{ fetchField })
 export default class TagEditorParam extends Component {
     props: Props;
 
+    componentWillMount() {
+        const { tag, fetchField } = this.props
+
+        if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
+            const fieldId = tag.dimension[1]
+            // Field values might already have been loaded so force the load of other field information too
+            fetchField(fieldId, true)
+        }
+    }
+
     setParameterAttribute(attr, val) {
         // only register an update if the value actually changes
         if (this.props.tag[attr] !== val) {
@@ -56,14 +75,14 @@ export default class TagEditorParam extends Component {
     }
 
     setDimension(fieldId) {
-        const { tag, onUpdate, databaseFields } = this.props;
+        const { tag, onUpdate, metadata } = this.props;
         const dimension = ["field-id", fieldId];
         if (!_.isEqual(tag.dimension !== dimension)) {
-            const field = _.findWhere(databaseFields, { id: fieldId });
+            const field = metadata.fields[dimension[1]]
             if (!field) {
                 return;
             }
-            const options = parameterOptionsForField(new Field(field));
+            const options = parameterOptionsForField(field);
             let widget_type;
             if (tag.widget_type && _.findWhere(options, { type: tag.widget_type })) {
                 widget_type = tag.widget_type;
@@ -79,22 +98,21 @@ export default class TagEditorParam extends Component {
     }
 
     render() {
-        const { tag, databaseFields } = this.props;
-
-        let dabaseHasSchemas = false;
-        if (databaseFields) {
-            let schemas = _.chain(databaseFields).pluck("schema").uniq().value();
-            dabaseHasSchemas = schemas.length > 1;
-        }
+        const { tag, database, databases, metadata } = this.props;
 
-        let widgetOptions;
+        let widgetOptions, table, fieldMetadataLoaded = false;
         if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
-            const field = _.findWhere(databaseFields, { id: tag.dimension[1] });
+            const field = metadata.fields[tag.dimension[1]]
+
             if (field) {
-                widgetOptions = parameterOptionsForField(new Field(field));
+                widgetOptions = parameterOptionsForField(field);
+                table = field.table
+                fieldMetadataLoaded = true
             }
         }
 
+        const isDimension = tag.type === "dimension"
+        const hasSelectedDimensionField = isDimension && Array.isArray(tag.dimension)
         return (
             <div className="pb2 mb2 border-bottom border-dark">
                 <h3 className="pb2">{tag.name}</h3>
@@ -129,27 +147,18 @@ export default class TagEditorParam extends Component {
                 { tag.type === "dimension" &&
                     <div className="pb1">
                         <h5 className="pb1 text-normal">{t`Field to map to`}</h5>
-                        <Select
-                            className="border-med bg-white block"
-                            value={Array.isArray(tag.dimension) ? tag.dimension[1] : null}
-                            onChange={(e) => this.setDimension(e.target.value)}
-                            searchProp="name"
-                            searchCaseInsensitive
-                            isInitiallyOpen={!tag.dimension}
-                            placeholder={t`Select…`}
-                            rowHeight={60}
-                            width={280}
-                        >
-                            {databaseFields && databaseFields.map(field =>
-                                <Option key={field.id} value={field.id} name={field.name}>
-                                    <div className="cursor-pointer">
-                                        <div className="h6 text-bold text-uppercase text-grey-2">{dabaseHasSchemas && (field.schema + " > ")}{field.table_name}</div>
-                                        <div className="h4 text-bold text-default">{field.name}</div>
-                                    </div>
-                                </Option>
-                            )}
-                        </Select>
 
+                        { (!hasSelectedDimensionField || (hasSelectedDimensionField && fieldMetadataLoaded)) &&
+                            <SchemaTableAndFieldDataSelector
+                                databases={databases}
+                                selectedDatabaseId={database.id}
+                                selectedTableId={table ? table.id : null}
+                                selectedFieldId={hasSelectedDimensionField ? tag.dimension[1] : null}
+                                setFieldFn={(fieldId) => this.setDimension(fieldId)}
+                                className="AdminSelect flex align-center"
+                                isInitiallyOpen={!tag.dimension}
+                            />
+                        }
                     </div>
                 }
 
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
index 9e4e378ab2a8ef81b60358c3322ecf8b5dcf4205..0d6b4e75208c9810ac3cf4c113ade2d7fe788d53 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
@@ -12,6 +12,7 @@ import cx from "classnames";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import type { DatasetQuery } from "metabase/meta/types/Card"
 import type { TableId } from "metabase/meta/types/Table"
+import type { Database } from "metabase/meta/types/Database"
 import type { TemplateTag } from "metabase/meta/types/Query"
 import type { Field as FieldObject } from "metabase/meta/types/Field"
 
@@ -22,6 +23,7 @@ type Props = {
     updateTemplateTag: (tag: TemplateTag) => void,
 
     databaseFields: FieldObject[],
+    databases: Database[],
     sampleDatasetId: TableId,
 
     onClose: () => void,
@@ -51,8 +53,10 @@ export default class TagEditorSidebar extends Component {
     }
 
     render() {
-        const { query } = this.props;
-        const tags = query.templateTags()
+        const { databases, databaseFields, sampleDatasetId, setDatasetQuery, query, updateTemplateTag, onClose } = this.props;
+        const tags = query.templateTags();
+        const databaseId = query.datasetQuery().database;
+        const database = databases.find(db => db.id === databaseId);
 
         let section;
         if (tags.length === 0) {
@@ -67,7 +71,7 @@ export default class TagEditorSidebar extends Component {
                     <h2 className="text-default">
                         {t`Variables`}
                     </h2>
-                    <a className="flex-align-right text-default text-brand-hover no-decoration" onClick={() => this.props.onClose()}>
+                    <a className="flex-align-right text-default text-brand-hover no-decoration" onClick={() => onClose()}>
                         <Icon name="close" size={18} />
                     </a>
                 </div>
@@ -77,9 +81,18 @@ export default class TagEditorSidebar extends Component {
                         <a className={cx("Button Button--small", { "Button--active": section === "help" })} onClick={() => this.setSection("help")}>{t`Help`}</a>
                     </div>
                     { section === "settings" ?
-                        <SettingsPane tags={tags} onUpdate={this.props.updateTemplateTag} databaseFields={this.props.databaseFields}/>
+                        <SettingsPane
+                            tags={tags}
+                            onUpdate={updateTemplateTag}
+                            databaseFields={databaseFields}
+                            database={database}
+                            databases={databases}
+                        />
                     :
-                        <TagEditorHelp sampleDatasetId={this.props.sampleDatasetId} setDatasetQuery={this.props.setDatasetQuery}/>
+                        <TagEditorHelp
+                            sampleDatasetId={sampleDatasetId}
+                            setDatasetQuery={setDatasetQuery}
+                        />
                     }
                 </div>
             </div>
@@ -87,11 +100,17 @@ export default class TagEditorSidebar extends Component {
     }
 }
 
-const SettingsPane = ({ tags, onUpdate, databaseFields }) =>
+const SettingsPane = ({ tags, onUpdate, databaseFields, database, databases }) =>
     <div>
         { tags.map(tag =>
             <div key={tags.name}>
-                <TagEditorParam tag={tag} onUpdate={onUpdate} databaseFields={databaseFields} />
+                <TagEditorParam
+                    tag={tag}
+                    onUpdate={onUpdate}
+                    databaseFields={databaseFields}
+                    database={database}
+                    databases={databases}
+                />
             </div>
         ) }
     </div>
diff --git a/frontend/src/metabase/redux/metadata.js b/frontend/src/metabase/redux/metadata.js
index 63b445f0ae103acdf1554702c03468282ec40b3f..1d0b4aa677f7d463aceeca45272fca934da00f82 100644
--- a/frontend/src/metabase/redux/metadata.js
+++ b/frontend/src/metabase/redux/metadata.js
@@ -277,6 +277,23 @@ export const fetchTableMetadata = createThunkAction(FETCH_TABLE_METADATA, functi
     };
 });
 
+export const FETCH_FIELD = "metabase/metadata/FETCH_FIELD";
+export const fetchField = createThunkAction(FETCH_FIELD, function(fieldId, reload) {
+    return async function(dispatch, getState) {
+        const requestStatePath = ["metadata", "fields", fieldId];
+        const existingStatePath = requestStatePath;
+        const getData = () => MetabaseApi.field_get({ fieldId })
+
+        return await fetchData({
+            dispatch,
+            getState,
+            requestStatePath,
+            existingStatePath,
+            getData,
+            reload: true
+        });
+    };
+});
 export const FETCH_FIELD_VALUES = "metabase/metadata/FETCH_FIELD_VALUES";
 export const fetchFieldValues = createThunkAction(FETCH_FIELD_VALUES, function(fieldId, reload) {
     return async function(dispatch, getState) {
@@ -480,6 +497,17 @@ export const fetchDatabasesWithMetadata = createThunkAction(FETCH_DATABASES_WITH
     };
 });
 
+const FETCH_REAL_DATABASES_WITH_METADATA = "metabase/metadata/FETCH_REAL_DATABASES_WITH_METADATA";
+export const fetchRealDatabasesWithMetadata = createThunkAction(FETCH_REAL_DATABASES_WITH_METADATA, (reload = false) => {
+    return async (dispatch, getState) => {
+        await dispatch(fetchRealDatabases())
+        const databases = getIn(getState(), ['metadata', 'databases']);
+        await Promise.all(Object.values(databases).map(database =>
+            dispatch(fetchDatabaseMetadata(database.id))
+        ));
+    };
+});
+
 const databases = handleActions({
 }, {});
 
@@ -491,6 +519,14 @@ const tables = handleActions({
 }, {});
 
 const fields = handleActions({
+    [FETCH_FIELD]: { next: (state, { payload: field }) =>
+        ({
+            ...state,
+            [field.id]: {
+                ...(state[field.id] || {}),
+                ...field
+            }
+        })},
     [FETCH_FIELD_VALUES]: { next: (state, { payload: fieldValues }) =>
         fieldValues ? assocIn(state, [fieldValues.field_id, "values"], fieldValues) : state },
     [ADD_PARAM_VALUES]: { next: (state, { payload: paramValues }) => {
diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
index 405115cd5e87d4b568091ad70da75975655a1fbf..69a3bf434cd185666da35af85bff87ca7f8fd8fa 100644
--- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
@@ -8,10 +8,10 @@ import S from "./GuideDetailEditor.css";
 
 import Select from "metabase/components/Select.jsx";
 import Icon from "metabase/components/Icon.jsx";
-import DataSelector from "metabase/query_builder/components/DataSelector.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
 import { typeToBgClass } from "../utils.js";
+import { SchemaTableAndSegmentDataSelector } from "metabase/query_builder/components/DataSelector";
 
 const GuideDetailEditor = ({
     className,
@@ -80,23 +80,20 @@ const GuideDetailEditor = ({
                         }}
                         placeholder={t`Select...`}
                     /> :
-                    <DataSelector
+                    <SchemaTableAndSegmentDataSelector
                         className={cx(selectClasses, 'inline-block', 'rounded', 'text-bold')}
                         triggerIconSize={12}
-                        includeTables={true}
-                        datasetQuery={{
-                            query: {
-                                source_table: formField.type.value === 'table' &&
-                                    Number.parseInt(formField.id.value)
-                            },
-                            database: (
-                                formField.type.value === 'table' &&
-                                tables[formField.id.value] &&
-                                tables[formField.id.value].db_id
-                            ) || Number.parseInt(Object.keys(databases)[0]),
-                            segment: formField.type.value === 'segment' &&
-                                Number.parseInt(formField.id.value)
-                        }}
+                        selectedTableId={
+                            formField.type.value === 'table' && Number.parseInt(formField.id.value)
+                        }
+                        selectedDatabaseId={
+                            formField.type.value === 'table' &&
+                            tables[formField.id.value] &&
+                            tables[formField.id.value].db_id
+                        }
+                        selectedSegmentId={
+                            formField.type.value === 'segment' && Number.parseInt(formField.id.value)
+                        }
                         databases={
                             Object.values(databases)
                                 .map(database => ({
@@ -114,8 +111,8 @@ const GuideDetailEditor = ({
                             const table = tables[tableId];
                             formField.id.onChange(table.id);
                             formField.type.onChange('table');
-                            formField.points_of_interest.onChange(table.points_of_interest || '');
-                            formField.caveats.onChange(table.caveats || '');
+                            formField.points_of_interest.onChange(table.points_of_interest || null);
+                            formField.caveats.onChange(table.caveats || null);
                         }}
                         segments={Object.values(segments)}
                         disabledSegmentIds={selectedIdTypePairs
diff --git a/frontend/src/metabase/reference/reference.js b/frontend/src/metabase/reference/reference.js
index 22e46676810f5b0e56e89d09b9ee96899951532b..3379a78a4ecebf4618c3ea727a49d839ef771e7d 100644
--- a/frontend/src/metabase/reference/reference.js
+++ b/frontend/src/metabase/reference/reference.js
@@ -102,7 +102,7 @@ export const wrappedFetchGuide = async (props) => {
                      props.fetchDashboards(),
                      props.fetchMetrics(),
                      props.fetchSegments(),
-                     props.fetchDatabasesWithMetadata()]
+                     props.fetchRealDatabasesWithMetadata()]
                 )}
         )()
 }
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index df8bdc4d69b6a083d0d83dc5b5e1730c607ae019..6ed0b7d7b961384f122ca07ad632e93e9fcb3672 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -142,7 +142,7 @@ export const MetabaseApi = {
     // table_sync_metadata:        POST("/api/table/:tableId/sync"),
     table_rescan_values:       POST("/api/table/:tableId/rescan_values"),
     table_discard_values:      POST("/api/table/:tableId/discard_values"),
-    // field_get:                   GET("/api/field/:fieldId"),
+    field_get:                   GET("/api/field/:fieldId"),
     // field_summary:               GET("/api/field/:fieldId/summary"),
     field_values:                GET("/api/field/:fieldId/values"),
     field_values_update:        POST("/api/field/:fieldId/values"),
diff --git a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
index f8716c37c1e832c9b1d0e7a5be0e1f898323fb56..6fea7453924b3e2eaa8a3f92f009c664c7939a90 100644
--- a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
+++ b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
@@ -172,7 +172,7 @@ const QUERY_BUILDER_STEPS = [
                     src="app/assets/img/qb_tutorial/boat.png" width={190}
                 />
                 <h3>{t`Well done!`}</h3>
-                <p>{t`That's all! If you still have questions, check out our`} <a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/start">{t`User's Guide`}</a>. {t`Have fun exploring your data!`}</p>
+                <p>{t`That's all! If you still have questions, check out our`} <a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/start.html">{t`User's Guide`}</a>. {t`Have fun exploring your data!`}</p>
                 <a className="Button Button--primary" onClick={props.onNext}>{t`Thanks`}!</a>
             </div>
     },
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 813ca105a0806c3b0e556c1c8fc43e3a2e3d2c97..04d4d6177bcde9b7f9764bef3a6ac7dfb846f3da 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -273,7 +273,8 @@ export default class TableInteractive extends Component {
         const isSortable = isClickable && column.source;
         const isRightAligned = isColumnRightAligned(column);
 
-        const isSorted = sort && sort[0] && sort[0][0] === column.id;
+        // the column id is in `["field-id", fieldId]` format
+        const isSorted = sort && sort[0] && sort[0][0] && sort[0][0][1] === column.id;
         const isAscending = sort && sort[0] && sort[0][1] === "ascending";
 
         return (
diff --git a/frontend/test/components/Calendar.unit.test.js b/frontend/test/components/Calendar.unit.test.js
index eab63d54f21893c36db88fbf4657f40610a0ee57..551d5fde3e8682e0ff562de482ade38ea68e91c5 100644
--- a/frontend/test/components/Calendar.unit.test.js
+++ b/frontend/test/components/Calendar.unit.test.js
@@ -21,6 +21,7 @@ describe("Calendar", () => {
     });
 
     it("should switch months correctly", () => {
+        mockDate.set('2018-01-12T12:00:00Z', 0);
         const calendar = mount(
             <Calendar selected={moment("2018-01-01")} onChange={() => {}}/>
         );
diff --git a/frontend/test/metabase-lib/Mode.unit.spec.js b/frontend/test/metabase-lib/Mode.unit.spec.js
index b111e62aa3dbbe5bbf33a623d9fca856640112c1..5cc71c8f8fb3d3ae1a4e71c97041c638cbe5f5b7 100644
--- a/frontend/test/metabase-lib/Mode.unit.spec.js
+++ b/frontend/test/metabase-lib/Mode.unit.spec.js
@@ -22,7 +22,7 @@ describe("Mode", () => {
         .mode();
 
     describe("forQuestion(question)", () => {
-        it("with structured query question", () => {
+        describe("with structured query question", () => {
             // testbed for generative testing? see http://leebyron.com/testcheck-js
 
             it("returns `segment` mode with raw data", () => {});
@@ -39,11 +39,11 @@ describe("Mode", () => {
             it("returns `default` mode with >=0 aggregations and >=3 breakouts", () => {});
             it("returns `default` mode with >=1 aggregations and >=1 breakouts when first neither date or category", () => {});
         });
-        it("with native query question", () => {
+        describe("with native query question", () => {
             it("returns `NativeMode` for empty query", () => {});
             it("returns `NativeMode` for query with query text", () => {});
         });
-        it("with oddly constructed query", () => {
+        describe("with oddly constructed query", () => {
             it("should throw an error", () => {
                 // this is not the actual behavior atm (it returns DefaultMode)
             });
diff --git a/frontend/test/parameters/parameters.integ.spec.js b/frontend/test/parameters/parameters.integ.spec.js
index 0048da534b65a6d97d5b484ab4478ac376848127..45839acbe735812fd602812f4df90833e2d0e95f 100644
--- a/frontend/test/parameters/parameters.integ.spec.js
+++ b/frontend/test/parameters/parameters.integ.spec.js
@@ -34,7 +34,7 @@ import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEdit
 import { delay } from "metabase/lib/promise";
 import TagEditorSidebar from "metabase/query_builder/components/template_tags/TagEditorSidebar";
 import { getQuery } from "metabase/query_builder/selectors";
-import { ADD_PARAM_VALUES, FETCH_FIELD_VALUES } from "metabase/redux/metadata";
+import { ADD_PARAM_VALUES, FETCH_TABLE_METADATA } from "metabase/redux/metadata";
 import RunButton from "metabase/query_builder/components/RunButton";
 import Scalar from "metabase/visualizations/visualizations/Scalar";
 import Parameters from "metabase/parameters/components/Parameters";
@@ -45,7 +45,10 @@ import SharingPane from "metabase/public/components/widgets/SharingPane";
 import { EmbedTitle } from "metabase/public/components/widgets/EmbedModalContent";
 import PreviewPane from "metabase/public/components/widgets/PreviewPane";
 import CopyWidget from "metabase/components/CopyWidget";
+import ListSearchField from "metabase/components/ListSearchField";
 import * as Urls from "metabase/lib/urls";
+import QuestionEmbedWidget from "metabase/query_builder/containers/QuestionEmbedWidget";
+import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
 async function updateQueryText(store, queryText) {
     // We don't have Ace editor so we have to trigger the Redux action manually
@@ -112,6 +115,7 @@ describe("parameters", () => {
             expect(enabledToggleContainer.text()).toBe("Enabled");
         });
 
+        // Note: Test suite is sequential, so individual test cases can't be run individually
         it("should allow users to create parameterized SQL questions", async () => {
             // Don't render Ace editor in tests because it uses many DOM methods that aren't supported by jsdom
             // NOTE Atte Keinänen 8/9/17: Ace provides a MockRenderer class which could be used for pseudo-rendering and
@@ -142,12 +146,23 @@ describe("parameters", () => {
 
             await delay(500);
 
-            setInputValue(tagEditorSidebar.find(".TestPopoverBody .AdminSelect").first(), "cat")
-            const categoryRow = tagEditorSidebar.find(".TestPopoverBody .ColumnarSelector-row").first();
-            expect(categoryRow.text()).toBe("ProductsCategory");
+            const productsRow = tagEditorSidebar.find(".TestPopoverBody .List-section").at(4).find("a");
+            expect(productsRow.text()).toBe("Products");
+            click(productsRow);
+
+            // Table fields should be loaded on-the-fly before showing the field selector
+            await store.waitForActions(FETCH_TABLE_METADATA)
+            // Needed due to state update after fetching metadata
+            await delay(100)
+
+            const searchField = tagEditorSidebar.find(".TestPopoverBody").find(ListSearchField).find("input").first()
+            setInputValue(searchField, "cat")
+
+            const categoryRow = tagEditorSidebar.find(".TestPopoverBody .List-section").at(2).find("a");
+            expect(categoryRow.text()).toBe("Category");
             click(categoryRow);
 
-            await store.waitForActions([UPDATE_TEMPLATE_TAG, FETCH_FIELD_VALUES])
+            await store.waitForActions([UPDATE_TEMPLATE_TAG])
 
             // close the template variable sidebar
             click(tagEditorSidebar.find(".Icon-close"));
@@ -180,7 +195,7 @@ describe("parameters", () => {
             await delay(500);
 
             // open sharing panel
-            click(app.find(".Icon-share"));
+            click(app.find(QuestionEmbedWidget).find(EmbedWidget));
 
             // "Embed this question in an application"
             click(app.find(SharingPane).find("h3").last());
diff --git a/frontend/test/pulse/pulse.integ.spec.js b/frontend/test/pulse/pulse.integ.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a010f105792d30ffd5f3be22297f8450611a5ad
--- /dev/null
+++ b/frontend/test/pulse/pulse.integ.spec.js
@@ -0,0 +1,162 @@
+
+import {
+    useSharedAdminLogin,
+    createTestStore,
+    createSavedQuestion
+} from "__support__/integrated_tests";
+import {
+    click,
+    setInputValue
+} from "__support__/enzyme_utils"
+
+import React from 'react';
+import { mount } from "enzyme";
+
+import {
+    CardApi,
+    PulseApi
+} from "metabase/services";
+import Question from "metabase-lib/lib/Question";
+
+import PulseListApp from "metabase/pulse/containers/PulseListApp";
+import PulseEditApp from "metabase/pulse/containers/PulseEditApp";
+import PulseListItem from "metabase/pulse/components/PulseListItem";
+import CardPicker from "metabase/pulse/components/CardPicker";
+import PulseCardPreview from "metabase/pulse/components/PulseCardPreview";
+import Toggle from "metabase/components/Toggle";
+
+import { FETCH_PULSES, SET_EDITING_PULSE, SAVE_EDITING_PULSE, FETCH_CARDS, FETCH_PULSE_CARD_PREVIEW } from "metabase/pulse/actions";
+
+describe("Pulse", () => {
+  let questionCount, questionRaw;
+  const normalFormInput = PulseApi.form_input
+
+  beforeAll(async () => {
+    useSharedAdminLogin()
+
+    const formInput = await PulseApi.form_input()
+    PulseApi.form_input = () => ({
+        channels: {
+        ...formInput.channels,
+            "email": {
+                ...formInput.channels.email,
+                "configured": true
+            }
+        }
+    })
+
+    questionCount = await createSavedQuestion(
+        Question.create({databaseId: 1, tableId: 1, metadata: null})
+            .query()
+            .addAggregation(["count"])
+            .question()
+            .setDisplay("scalar")
+            .setDisplayName("count")
+    )
+
+    questionRaw = await createSavedQuestion(
+        Question.create({databaseId: 1, tableId: 1, metadata: null})
+            .query()
+            .question()
+            .setDisplay("table")
+            .setDisplayName("table")
+    )
+
+    // possibly not necessary, but just to be sure we start with clean slate
+    for (const pulse of await PulseApi.list()) {
+      await PulseApi.delete({ pulseId: pulse.id })
+    }
+  })
+
+  afterAll(async () => {
+    PulseApi.form_input = normalFormInput
+
+    await CardApi.delete({ cardId: questionCount.id() })
+    await CardApi.delete({ cardId: questionRaw.id() })
+
+    for (const pulse of await PulseApi.list()) {
+      await PulseApi.delete({ pulseId: pulse.id })
+    }
+  })
+
+  let store;
+  beforeEach(async () => {
+    store = await createTestStore()
+  })
+
+  it("should load pulses", async () => {
+    store.pushPath("/pulse");
+    const app = mount(store.connectContainer(<PulseListApp />));
+    await store.waitForActions([FETCH_PULSES]);
+
+    const items = app.find(PulseListItem)
+    expect(items.length).toBe(0)
+  })
+
+  it("should load create pulse", async () => {
+    store.pushPath("/pulse/create");
+    const app = mount(store.connectContainer(<PulseEditApp />));
+    await store.waitForActions([SET_EDITING_PULSE,FETCH_CARDS]);
+
+    // no previews yet
+    expect(app.find(PulseCardPreview).length).toBe(0)
+
+    // set name to 'foo'
+    setInputValue(app.find("input").first(), "foo")
+
+    // email channel should be enabled
+    expect(app.find(Toggle).first().props().value).toBe(true);
+
+    // add count card
+    app.find(CardPicker).first().props().onChange(questionCount.id())
+    await store.waitForActions([FETCH_PULSE_CARD_PREVIEW]);
+
+    // add raw card
+    app.find(CardPicker).first().props().onChange(questionRaw.id())
+    await store.waitForActions([FETCH_PULSE_CARD_PREVIEW]);
+
+    let previews = app.find(PulseCardPreview);
+    expect(previews.length).toBe(2)
+
+    // NOTE: check text content since enzyme doesn't doesn't seem to work well with dangerouslySetInnerHTML
+    expect(previews.at(0).text()).toBe("count12,805")
+    expect(previews.at(0).find(".Icon-attachment").length).toBe(1)
+    expect(previews.at(1).text()).toBe("tableThis question will be added as a file attachment")
+    expect(previews.at(1).find(".Icon-attachment").length).toBe(0)
+
+    // toggle email channel off
+    click(app.find(Toggle).first())
+
+    previews = app.find(PulseCardPreview);
+    expect(previews.at(0).text()).toBe("count12,805")
+    expect(previews.at(0).find(".Icon-attachment").length).toBe(0)
+    expect(previews.at(1).text()).toBe("tableThis question won't be included in your Pulse")
+    expect(previews.at(1).find(".Icon-attachment").length).toBe(0)
+
+    // toggle email channel on
+    click(app.find(Toggle).first())
+
+    // save
+    const saveButton = app.find(".PulseEdit-footer .Button").first();
+    expect(saveButton.hasClass("Button--primary")).toBe(true)
+    click(saveButton)
+
+    await store.waitForActions([SAVE_EDITING_PULSE]);
+
+    const [pulse] = await PulseApi.list();
+    expect(pulse.name).toBe("foo");
+    expect(pulse.cards[0].id).toBe(questionCount.id());
+    expect(pulse.cards[1].id).toBe(questionRaw.id());
+    expect(pulse.channels[0].channel_type).toBe("email");
+    expect(pulse.channels[0].enabled).toBe(true);
+  })
+
+  it("should load pulses", async () => {
+    store.pushPath("/pulse");
+    const app = mount(store.connectContainer(<PulseListApp />));
+    await store.waitForActions([FETCH_PULSES]);
+
+    const items = app.find(PulseListItem)
+    expect(items.length).toBe(1)
+  })
+})
diff --git a/package.json b/package.json
index 808b5a093930b99855744fbcad90a0676c9ec652..e85fcafe8ca59612fce3752093d57c754e055d9e 100644
--- a/package.json
+++ b/package.json
@@ -158,7 +158,7 @@
     "test": "yarn run test-integrated && yarn run test-unit && yarn run test-karma",
     "test-integrated": "babel-node ./frontend/test/__runner__/run_integrated_tests.js",
     "test-integrated-watch": "babel-node ./frontend/test/__runner__/run_integrated_tests.js --watch",
-    "test-unit": "jest --maxWorkers=10 --config jest.unit.conf.json --coverage",
+    "test-unit": "jest --maxWorkers=8 --config jest.unit.conf.json --coverage",
     "test-unit-watch": "jest --maxWorkers=10 --config jest.unit.conf.json --watch",
     "test-unit-update-snapshot": "jest --maxWorkers=10 --config jest.unit.conf.json --updateSnapshot",
     "test-karma": "karma start frontend/test/karma.conf.js --single-run",
diff --git a/resources/frontend_client/app/assets/img/attachment.png b/resources/frontend_client/app/assets/img/attachment.png
new file mode 100644
index 0000000000000000000000000000000000000000..7a8921211d82ef40b56d6185de1add871f89cf01
Binary files /dev/null and b/resources/frontend_client/app/assets/img/attachment.png differ
diff --git a/resources/frontend_client/app/assets/img/attachment@2x.png b/resources/frontend_client/app/assets/img/attachment@2x.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5f027fa07df58f5f016932dfc41a5c6e1706539
Binary files /dev/null and b/resources/frontend_client/app/assets/img/attachment@2x.png differ
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index 7b379e5a57d85d8c2e70f88d91b0b7ccf6d33d1c..1c99056f602d6848532f578595d588a8abf39440 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -3998,3 +3998,25 @@ databaseChangeLog:
             tableName: report_dashboardcard
             columnName: card_id
             columnDataType: int
+  - changeSet:
+      id: 72
+      author: senior
+      comment: 'Added 0.28.0'
+      changes:
+        - addColumn:
+            tableName: pulse_card
+            columns:
+              - column:
+                  name: include_csv
+                  type: boolean
+                  defaultValueBoolean: false
+                  remarks: 'True if a CSV of the data should be included for this pulse card'
+                  constraints:
+                    nullable: false
+              - column:
+                  name: include_xls
+                  type: boolean
+                  defaultValueBoolean: false
+                  remarks: 'True if a XLS of the data should be included for this pulse card'
+                  constraints:
+                    nullable: false
diff --git a/src/metabase/api/alert.clj b/src/metabase/api/alert.clj
index 7666eff1e57198db5ac3653cb4a995743cd3c165..d5f7fe6f18ed9e581e9070750f7cdabeab809590 100644
--- a/src/metabase/api/alert.clj
+++ b/src/metabase/api/alert.clj
@@ -107,6 +107,11 @@
     (doseq [recipient (non-creator-recipients alert)]
       (messages/send-you-were-added-alert-email! alert recipient @api/*current-user*))))
 
+(defn- maybe-include-csv [card alert-condition]
+  (if (= "rows" alert-condition)
+    (assoc card :include_csv true)
+    card))
+
 (api/defendpoint POST "/"
   "Create a new alert (`Pulse`)"
   [:as {{:keys [alert_condition card channels alert_first_only alert_above_goal] :as req} :body}]
@@ -116,10 +121,11 @@
    card              su/Map
    channels          (su/non-empty [su/Map])}
   (pulse-api/check-card-read-permissions [card])
-  (let [new-alert (api/check-500
+  (let [alert-card (-> card (maybe-include-csv alert_condition) pulse/create-card-ref)
+        new-alert (api/check-500
                    (-> req
                        only-alert-keys
-                       (pulse/create-alert! api/*current-user-id* (u/get-id card) channels)))]
+                       (pulse/create-alert! api/*current-user-id* alert-card channels)))]
 
     (notify-new-alert-created! new-alert)
 
@@ -153,7 +159,7 @@
         _             (check-alert-update-permissions old-alert)
         updated-alert (-> req
                           only-alert-keys
-                          (assoc :id id :card (u/get-id card) :channels channels)
+                          (assoc :id id :card (pulse/create-card-ref card) :channels channels)
                           pulse/update-alert!)]
 
     ;; Only admins can update recipients
diff --git a/src/metabase/api/pulse.clj b/src/metabase/api/pulse.clj
index 81d5e852158ed531d6d0f2b6ff6e57979597c490..f6106dcdad95141c591c1388597025dc47d73a50 100644
--- a/src/metabase/api/pulse.clj
+++ b/src/metabase/api/pulse.clj
@@ -18,6 +18,7 @@
              [pulse-channel :refer [channel-types]]]
             [metabase.pulse.render :as render]
             [metabase.util.schema :as su]
+            [metabase.util.urls :as urls]
             [schema.core :as s]
             [toucan.db :as db])
   (:import java.io.ByteArrayInputStream
@@ -49,7 +50,7 @@
    channels      (su/non-empty [su/Map])
    skip_if_empty s/Bool}
   (check-card-read-permissions cards)
-  (api/check-500 (pulse/create-pulse! name api/*current-user-id* (map u/get-id cards) channels skip_if_empty)))
+  (api/check-500 (pulse/create-pulse! name api/*current-user-id* (map pulse/create-card-ref cards) channels skip_if_empty)))
 
 
 (api/defendpoint GET "/:id"
@@ -70,7 +71,7 @@
   (check-card-read-permissions cards)
   (pulse/update-pulse! {:id             id
                         :name           name
-                        :cards          (map u/get-id cards)
+                        :cards          (map pulse/create-card-ref cards)
                         :channels       channels
                         :skip-if-empty? skip_if_empty})
   (pulse/retrieve-pulse id))
@@ -130,6 +131,8 @@
     {:id              id
      :pulse_card_type card-type
      :pulse_card_html card-html
+     :pulse_card_name (:name card)
+     :pulse_card_url  (urls/card-url (:id card))
      :row_count       (:row_count result)}))
 
 (api/defendpoint GET "/preview_card_png/:id"
diff --git a/src/metabase/api/table.clj b/src/metabase/api/table.clj
index 9e4830c28c8606ff1a3fcae3c05ebc1e3c557281..e2840ee2ef8441777af83ec8be19707f5e3f0b15 100644
--- a/src/metabase/api/table.clj
+++ b/src/metabase/api/table.clj
@@ -166,12 +166,18 @@
 (defn- supports-numeric-binning? [driver]
   (and driver (contains? (driver/features driver) :binning)))
 
+(defn- supports-date-binning?
+  "Time fields don't support binning, returns true if it's a DateTime field and not a time field"
+  [{:keys [base_type special_type]}]
+  (and (or (isa? base_type :type/DateTime)
+           (isa? special_type :type/DateTime))
+       (not (isa? base_type :type/Time))))
+
 (defn- assoc-field-dimension-options [driver {:keys [base_type special_type fingerprint] :as field}]
   (let [{min_value :min, max_value :max} (get-in fingerprint [:type :type/Number])
         [default-option all-options] (cond
 
-                                       (or (isa? base_type :type/DateTime)
-                                           (isa? special_type :type/DateTime))
+                                       (supports-date-binning? field)
                                        [date-default-index datetime-dimension-indexes]
 
                                        (and min_value max_value
@@ -214,7 +220,7 @@
   [id include_sensitive_fields]
   {include_sensitive_fields (s/maybe su/BooleanString)}
   (let [table (api/read-check Table id)
-        driver (driver/engine->driver (db/select-one-field :engine Database :id (:db_id table)))]
+        driver (driver/database-id->driver (:db_id table))]
     (-> table
         (hydrate :db [:fields :target :dimensions] :segments :metrics)
         (update :fields with-normal-values)
@@ -231,22 +237,26 @@
 (defn- card-result-metadata->virtual-fields
   "Return a sequence of 'virtual' fields metadata for the 'virtual' table for a Card in the Saved Questions 'virtual'
    database."
-  [card-id metadata]
-  (for [col metadata]
-    (assoc col
-      :table_id     (str "card__" card-id)
-      :id           [:field-literal (:name col) (or (:base_type col) :type/*)]
-      ;; don't return :special_type if it's a PK or FK because it confuses the frontend since it can't actually be
-      ;; used that way IRL
-      :special_type (when-let [special-type (keyword (:special_type col))]
-                      (when-not (or (isa? special-type :type/PK)
-                                    (isa? special-type :type/FK))
-                        special-type)))))
+  [card-id database-id metadata]
+  (let [add-field-dimension-options #(assoc-field-dimension-options (driver/database-id->driver database-id) %)]
+    (for [col metadata]
+      (-> col
+          (update :base_type keyword)
+          (assoc
+              :table_id     (str "card__" card-id)
+              :id           [:field-literal (:name col) (or (:base_type col) :type/*)]
+              ;; don't return :special_type if it's a PK or FK because it confuses the frontend since it can't actually be
+              ;; used that way IRL
+              :special_type (when-let [special-type (keyword (:special_type col))]
+                              (when-not (or (isa? special-type :type/PK)
+                                            (isa? special-type :type/FK))
+                                special-type)))
+          add-field-dimension-options))))
 
 (defn card->virtual-table
   "Return metadata for a 'virtual' table for a CARD in the Saved Questions 'virtual' database. Optionally include
    'virtual' fields as well."
-  [card & {:keys [include-fields?]}]
+  [{:keys [database_id] :as card} & {:keys [include-fields?]}]
   ;; if collection isn't already hydrated then do so
   (let [card (hydrate card :colllection)]
     (cond-> {:id           (str "card__" (u/get-id card))
@@ -254,14 +264,17 @@
              :display_name (:name card)
              :schema       (get-in card [:collection :name] "Everything else")
              :description  (:description card)}
-      include-fields? (assoc :fields (card-result-metadata->virtual-fields (u/get-id card) (:result_metadata card))))))
+      include-fields? (assoc :fields (card-result-metadata->virtual-fields (u/get-id card) database_id (:result_metadata card))))))
 
 (api/defendpoint GET "/card__:id/query_metadata"
   "Return metadata for the 'virtual' table for a Card."
   [id]
-  (-> (db/select-one [Card :id :dataset_query :result_metadata :name :description :collection_id], :id id)
-      api/read-check
-      (card->virtual-table :include-fields? true)))
+  (let [{:keys [database_id] :as card } (db/select-one [Card :id :dataset_query :result_metadata :name :description :collection_id :database_id]
+                                          :id id)]
+    (-> card
+        api/read-check
+        (card->virtual-table :include-fields? true)
+        (assoc-dimension-options (driver/database-id->driver database_id)))))
 
 (api/defendpoint GET "/card__:id/fks"
   "Return FK info for the 'virtual' table for a Card. This is always empty, so this endpoint
diff --git a/src/metabase/cmd.clj b/src/metabase/cmd.clj
new file mode 100644
index 0000000000000000000000000000000000000000..92a17ce113c73dc822f802372b51531924f224fc
--- /dev/null
+++ b/src/metabase/cmd.clj
@@ -0,0 +1,131 @@
+(ns metabase.cmd
+  "Functions for commands that can be ran from the command-line with `lein` or the Metabase JAR. These are ran as
+  follows:
+
+    <metabase> <command> <options>
+
+  for example, running the `migrate` command and passing it `force` can be done using one of the following ways:
+
+    lein run migrate force
+    java -jar metabase.jar migrate force
+
+
+  Logic below translates resolves the command itself to a function marked with `^:command` metadata and calls the
+  function with arguments as appropriate.
+
+  You can see what commands are available by running the command `help`. This command uses the docstrings and arglists
+  associated with each command's entrypoint function to generate descriptions for each command."
+  (:require [clojure.string :as str]
+            [metabase
+             [config :as config]
+             [db :as mdb]
+             [util :as u]]))
+
+(defn ^:command migrate
+  "Run database migrations. Valid options for DIRECTION are `up`, `force`, `down-one`, `print`, or `release-locks`."
+  [direction]
+  (mdb/migrate! (keyword direction)))
+
+(defn ^:command load-from-h2
+  "Transfer data from existing H2 database to the newly created MySQL or Postgres DB specified by env vars."
+  ([]
+   (load-from-h2 nil))
+  ([h2-connection-string]
+   (require 'metabase.cmd.load-from-h2)
+   (binding [mdb/*disable-data-migrations* true]
+     ((resolve 'metabase.cmd.load-from-h2/load-from-h2!) h2-connection-string))))
+
+(defn ^:command profile
+  "Start Metabase the usual way and exit. Useful for profiling Metabase launch time."
+  []
+  ;; override env var that would normally make Jetty block forever
+  (require 'environ.core)
+  (intern 'environ.core 'env (assoc environ.core/env :mb-jetty-join "false"))
+  (u/profile "start-normally" ((resolve 'metabase.core/start-normally))))
+
+(defn ^:command reset-password
+  "Reset the password for a user with EMAIL-ADDRESS."
+  [email-address]
+  (require 'metabase.cmd.reset-password)
+  ((resolve 'metabase.cmd.reset-password/reset-password!) email-address))
+
+(defn ^:command help
+  "Show this help message listing valid Metabase commands."
+  []
+  (println "Valid commands are:")
+  (doseq [[symb varr] (sort (ns-interns 'metabase.cmd))
+          :when       (:command (meta varr))]
+    (println symb (str/join " " (:arglists (meta varr))))
+    (println "\t" (when-let [dox (:doc (meta varr))]
+                    (str/replace dox #"\s+" " ")))) ; replace newlines or multiple spaces with single spaces
+  (println "\nSome other commands you might find useful:\n")
+  (println "java -cp metabase.jar org.h2.tools.Shell -url jdbc:h2:/path/to/metabase.db")
+  (println "\tOpen an SQL shell for the Metabase H2 DB"))
+
+(defn ^:command version
+  "Print version information about Metabase and the current system."
+  []
+  (println "Metabase version:" config/mb-version-info)
+  (println "\nOS:"
+           (System/getProperty "os.name")
+           (System/getProperty "os.version")
+           (System/getProperty "os.arch"))
+  (println "\nJava version:"
+           (System/getProperty "java.vm.name")
+           (System/getProperty "java.version"))
+  (println "\nCountry:"       (System/getProperty "user.country"))
+  (println "System timezone:" (System/getProperty "user.timezone"))
+  (println "Language:"        (System/getProperty "user.language"))
+  (println "File encoding:"   (System/getProperty "file.encoding")))
+
+(defn ^:command api-documentation
+  "Generate a markdown file containing documentation for all API endpoints. This is written to a file called
+  `docs/api-documentation.md`."
+  []
+  (require 'metabase.cmd.endpoint-dox)
+  ((resolve 'metabase.cmd.endpoint-dox/generate-dox!)))
+
+(defn ^:command check-i18n
+  "Run normally, but with fake translations in place for all user-facing backend strings. Useful for checking what
+  things need to be translated."
+  []
+  (println "Swapping out implementation of puppetlabs.i18n.core/fmt...")
+  (require 'puppetlabs.i18n.core)
+  (let [orig-fn @(resolve 'puppetlabs.i18n.core/fmt)]
+    (intern 'puppetlabs.i18n.core 'fmt (comp str/reverse orig-fn)))
+  (println "Ok.")
+  (println "Reloading all Metabase namespaces...")
+  (let [namespaces-to-reload (for [ns-symb @u/metabase-namespace-symbols
+                                   :when (and (not (#{'metabase.cmd 'metabase.core} ns-symb))
+                                              (u/ignore-exceptions
+                                                ;; try to resolve namespace. If it's not loaded yet, this will throw
+                                                ;; an Exception, so we can skip reloading it
+                                                (the-ns ns-symb)))]
+                               ns-symb)]
+    (apply require (conj (vec namespaces-to-reload) :reload)))
+  (println "Ok.")
+  (println "Starting normally with swapped i18n strings...")
+  ((resolve 'metabase.core/start-normally)))
+
+
+;;; ------------------------------------------------ Running Commands ------------------------------------------------
+
+(defn- cmd->fn [command-name]
+  (or (when (seq command-name)
+        (when-let [varr (ns-resolve 'metabase.cmd (symbol command-name))]
+          (when (:command (meta varr))
+            @varr)))
+      (do (println (u/format-color 'red "Unrecognized command: %s" command-name))
+          (help)
+          (System/exit 1))))
+
+(defn run-cmd
+  "Run `cmd` with `args`. This is a function above. e.g. `lein run metabase migrate force` becomes
+  `(migrate \"force\")`."
+  [cmd args]
+  (try (apply (cmd->fn cmd) args)
+       (catch Throwable e
+         (.printStackTrace e)
+         (println (u/format-color 'red "Command failed with exception: %s" (.getMessage e)))
+         (System/exit 1)))
+  (System/exit 0))
diff --git a/src/metabase/core.clj b/src/metabase/core.clj
index a0eef0a372517bd8de8ab1480ee44dca33abb8cc..a810959eff3d2773f879f706eb52a9d02a526530 100644
--- a/src/metabase/core.clj
+++ b/src/metabase/core.clj
@@ -2,11 +2,8 @@
 (ns metabase.core
   (:gen-class)
   (:require [cheshire.core :as json]
-            [clojure
-             [pprint :as pprint]
-             [string :as s]]
+            [clojure.pprint :as pprint]
             [clojure.tools.logging :as log]
-            environ.core
             [medley.core :as m]
             [metabase
              [config :as config]
@@ -255,86 +252,9 @@
       (log/error "Metabase Initialization FAILED: " (.getMessage e))
       (System/exit 1))))
 
-;;; ---------------------------------------- Special Commands ----------------------------------------
-
-(defn ^:command migrate
-  "Run database migrations. Valid options for DIRECTION are `up`, `force`, `down-one`, `print`, or `release-locks`."
-  [direction]
-  (mdb/migrate! (keyword direction)))
-
-(defn ^:command load-from-h2
-  "Transfer data from existing H2 database to the newly created MySQL or Postgres DB specified by env vars."
-  ([]
-   (load-from-h2 nil))
-  ([h2-connection-string]
-   (require 'metabase.cmd.load-from-h2)
-   (binding [mdb/*disable-data-migrations* true]
-     ((resolve 'metabase.cmd.load-from-h2/load-from-h2!) h2-connection-string))))
-
-(defn ^:command profile
-  "Start Metabase the usual way and exit. Useful for profiling Metabase launch time."
-  []
-  ;; override env var that would normally make Jetty block forever
-  (intern 'environ.core 'env (assoc environ.core/env :mb-jetty-join "false"))
-  (u/profile "start-normally" (start-normally)))
-
-(defn ^:command reset-password
-  "Reset the password for a user with EMAIL-ADDRESS."
-  [email-address]
-  (require 'metabase.cmd.reset-password)
-  ((resolve 'metabase.cmd.reset-password/reset-password!) email-address))
-
-(defn ^:command help
-  "Show this help message listing valid Metabase commands."
-  []
-  (println "Valid commands are:")
-  (doseq [[symb varr] (sort (ns-interns 'metabase.core))
-          :when       (:command (meta varr))]
-    (println symb (s/join " " (:arglists (meta varr))))
-    (println "\t" (:doc (meta varr))))
-  (println "\nSome other commands you might find useful:\n")
-  (println "java -cp metabase.jar org.h2.tools.Shell -url jdbc:h2:/path/to/metabase.db")
-  (println "\tOpen an SQL shell for the Metabase H2 DB"))
-
-(defn ^:command version
-  "Print version information about Metabase and the current system."
-  []
-  (println "Metabase version:" config/mb-version-info)
-  (println "\nOS:"
-           (System/getProperty "os.name")
-           (System/getProperty "os.version")
-           (System/getProperty "os.arch"))
-  (println "\nJava version:"
-           (System/getProperty "java.vm.name")
-           (System/getProperty "java.version"))
-  (println "\nCountry:" (System/getProperty "user.country"))
-  (println "System timezone:" (System/getProperty "user.timezone"))
-  (println "Language:" (System/getProperty "user.language"))
-  (println "File encoding:" (System/getProperty "file.encoding")))
-
-(defn ^:command api-documentation
-  "Generate a markdown file containing documentation for all API endpoints. This is written to a file called `docs/api-documentation.md`."
-  []
-  (require 'metabase.cmd.endpoint-dox)
-  ((resolve 'metabase.cmd.endpoint-dox/generate-dox!)))
-
-
-(defn- cmd->fn [command-name]
-  (or (when (seq command-name)
-        (when-let [varr (ns-resolve 'metabase.core (symbol command-name))]
-          (when (:command (meta varr))
-            @varr)))
-      (do (println (u/format-color 'red "Unrecognized command: %s" command-name))
-          (help)
-          (System/exit 1))))
-
-(defn- run-cmd [cmd & args]
-  (try (apply (cmd->fn cmd) args)
-       (catch Throwable e
-         (.printStackTrace e)
-         (println (u/format-color 'red "Command failed with exception: %s" (.getMessage e)))
-         (System/exit 1)))
-  (System/exit 0))
+(defn- run-cmd [cmd args]
+  (require 'metabase.cmd)
+  ((resolve 'metabase.cmd/run-cmd) cmd args))
 
 
 ;;; ---------------------------------------- App Entry Point ----------------------------------------
@@ -343,5 +263,5 @@
   "Launch Metabase in standalone mode."
   [& [cmd & args]]
   (if cmd
-    (apply run-cmd cmd args) ; run a command like `java -jar metabase.jar migrate release-locks` or `lein run migrate release-locks`
-    (start-normally)))       ; with no command line args just start Metabase normally
+    (run-cmd cmd args) ; run a command like `java -jar metabase.jar migrate release-locks` or `lein run migrate release-locks`
+    (start-normally))) ; with no command line args just start Metabase normally
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index fee72085c32bdeb6207d55a78483cc9c205bb867..920fe3e535fb63f7a6ab881875ce1fc4d55ba22e 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -180,7 +180,7 @@
                  SELECT * FROM my_table\"}")
 
   (notify-database-updated [this, ^DatabaseInstance database]
-    "*OPTIONAL*. Notify the driver that the attributes of the DATABASE have changed.  This is specifically relevant in
+    "*OPTIONAL*. Notify the driver that the attributes of the DATABASE have changed. This is specifically relevant in
      the event that the driver was doing some caching or connection pooling.")
 
   (process-query-in-context [this, ^clojure.lang.IFn qp]
diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj
index 0c6ea6c87c7f33e1a5eab232cd8d8fe3e2447e29..16b631df0372476f35b57b76bbac6cd9a4dc7f09 100644
--- a/src/metabase/driver/bigquery.clj
+++ b/src/metabase/driver/bigquery.clj
@@ -1,5 +1,9 @@
 (ns metabase.driver.bigquery
-  (:require [clojure
+  (:require [clj-time
+             [coerce :as tcoerce]
+             [core :as time]
+             [format :as tformat]]
+            [clojure
              [set :as set]
              [string :as str]
              [walk :as walk]]
@@ -18,8 +22,7 @@
             [metabase.driver.generic-sql.util.unprepare :as unprepare]
             [metabase.models
              [database :refer [Database]]
-             [field :as field]
-             [table :as table]]
+             [field :as field]]
             [metabase.query-processor.util :as qputil]
             [metabase.util.honeysql-extensions :as hx]
             [toucan.db :as db])
@@ -27,8 +30,9 @@
            [com.google.api.services.bigquery Bigquery Bigquery$Builder BigqueryScopes]
            [com.google.api.services.bigquery.model QueryRequest QueryResponse Table TableCell TableFieldSchema
             TableList TableList$Tables TableReference TableRow TableSchema]
+           java.sql.Time
            [java.util Collections Date]
-           [metabase.query_processor.interface DateTimeValue Value]))
+           [metabase.query_processor.interface DateTimeValue TimeValue Value]))
 
 (defrecord BigQueryDriver []
   clojure.lang.Named
@@ -103,6 +107,7 @@
     "DATE"      :type/Date
     "DATETIME"  :type/DateTime
     "TIMESTAMP" :type/DateTime
+    "TIME"      :type/Time
     :type/*))
 
 (defn- table-schema->metabase-field-info [^TableSchema schema]
@@ -148,6 +153,19 @@
                      (.getDSTSavings default-timezone)
                      (.getRawOffset  default-timezone)))))
 
+(def ^:private bigquery-time-format (tformat/formatter "HH:mm:SS" time/utc))
+
+(defn- parse-bigquery-time [time-string]
+  (->> time-string
+       (tformat/parse bigquery-time-format)
+       tcoerce/to-long
+       Time.))
+
+(defn- unparse-bigquery-time [coercible-to-dt]
+  (->> coercible-to-dt
+       tcoerce/to-date-time
+       (tformat/unparse bigquery-time-format)))
+
 (def ^:private type->parser
   "Functions that should be used to coerce string values in responses to the appropriate type for their column."
   {"BOOLEAN"   #(Boolean/parseBoolean %)
@@ -157,7 +175,8 @@
    "STRING"    identity
    "DATE"      parse-timestamp-str
    "DATETIME"  parse-timestamp-str
-   "TIMESTAMP" parse-timestamp-str})
+   "TIMESTAMP" parse-timestamp-str
+   "TIME"      parse-bigquery-time})
 
 (defn- post-process-native
   ([^QueryResponse response]
@@ -300,7 +319,6 @@
                   (update results :columns (partial map keyword)))]
     (assoc results :annotate? mbql?)))
 
-
 ;; These provide implementations of `->honeysql` that prevents HoneySQL from converting forms to prepared
 ;; statement parameters (`?`)
 (defmethod sqlqp/->honeysql [BigQueryDriver String]
@@ -316,6 +334,12 @@
   [_ date]
   (hsql/call :timestamp (hx/literal (u/date->iso-8601 date))))
 
+(defmethod sqlqp/->honeysql [BigQueryDriver TimeValue]
+  [driver {:keys [value]}]
+  (->> value
+       unparse-bigquery-time
+       (sqlqp/->honeysql driver)
+       hx/->time))
 
 (defn- field->alias [{:keys [^String schema-name, ^String field-name, ^String table-name, ^Integer index, field], :as this}]
   {:pre [(map? this) (or field
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index c5d6cee3d8795b77dc35093a8dea95f80a6d3007..2164bb60e278a7fd0b20678ce62e20b996c9860e 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -20,7 +20,7 @@
            [java.sql PreparedStatement ResultSet ResultSetMetaData SQLException]
            [java.util Calendar Date TimeZone]
            [metabase.query_processor.interface AgFieldRef BinnedField DateTimeField DateTimeValue Expression
-            ExpressionRef Field FieldLiteral RelativeDateTimeValue Value]))
+            ExpressionRef Field FieldLiteral RelativeDateTimeValue TimeField TimeValue Value]))
 
 (def ^:dynamic *query*
   "The outer query currently being processed."
@@ -72,7 +72,6 @@
      (nth (:aggregation query) index)
      (recur index (:source-query query) (dec aggregation-level)))))
 
-
 (defmulti ^{:doc          (str "Return an appropriate HoneySQL form for an object. Dispatches off both driver and object "
                                "classes making this easy to override in any places needed for a given driver.")
             :arglists     '([driver x])
@@ -111,6 +110,10 @@
   [driver {unit :unit, field :field}]
   (sql/date driver unit (->honeysql driver field)))
 
+(defmethod ->honeysql [Object TimeField]
+  [driver {field :field}]
+  (->honeysql driver field))
+
 (defmethod ->honeysql [Object BinnedField]
   [driver {:keys [bin-width min-value max-value field]}]
   (let [honeysql-field-form (->honeysql driver field)]
@@ -150,6 +153,9 @@
                                 (sql/current-datetime-fn driver)
                                 (driver/date-interval driver unit amount))))
 
+(defmethod ->honeysql [Object TimeValue]
+  [driver  {:keys [value]}]
+  (->honeysql driver value))
 
 ;;; ## Clause Handlers
 
@@ -409,6 +415,9 @@
     (mapv (fn [^Integer i value]
             (cond
 
+              (and tz (instance? java.sql.Time value))
+              (.setTime stmt i value (Calendar/getInstance tz))
+
               (and tz (instance? java.sql.Timestamp value))
               (.setTimestamp stmt i value (Calendar/getInstance tz))
 
diff --git a/src/metabase/driver/generic_sql/util/unprepare.clj b/src/metabase/driver/generic_sql/util/unprepare.clj
index 56845e692a68a5e0a3500406c0c5609ebed4ffcd..55b028dca9159ee2ac2b17f2a4696058bb16aa6a 100644
--- a/src/metabase/driver/generic_sql/util/unprepare.clj
+++ b/src/metabase/driver/generic_sql/util/unprepare.clj
@@ -4,17 +4,22 @@
             [honeysql.core :as hsql]
             [metabase.util :as u]
             [metabase.util.honeysql-extensions :as hx])
-  (:import java.util.Date))
+  (:import java.sql.Time
+           java.util.Date))
 
 (defprotocol ^:private IUnprepare
   (^:private unprepare-arg ^String [this settings]))
 
+(defn- unprepare-date [date-or-time iso-8601-fn]
+  (hsql/call iso-8601-fn (hx/literal (u/date->iso-8601 date-or-time))))
+
 (extend-protocol IUnprepare
   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)))))))
+  Date    (unprepare-arg [this {:keys [iso-8601-fn]}] (first (hsql/format (unprepare-date this iso-8601-fn))))
+  Time    (unprepare-arg [this {:keys [iso-8601-fn]}] (first (hsql/format (hx/->time (unprepare-date this iso-8601-fn))))))
 
 (defn unprepare
   "Convert a normal SQL `[statement & prepared-statement-args]` vector into a flat, non-prepared statement."
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index d65a4e8513c0f8e93e55e80c7eb59f14f4cd9e6a..13d1cbdd78bdb7facd1fc741b71fa181be82c341 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -16,7 +16,8 @@
             [metabase.util
              [honeysql-extensions :as hx]
              [ssh :as ssh]])
-  (:import [java.util Date TimeZone]
+  (:import java.sql.Time
+           [java.util Date TimeZone]
            org.joda.time.format.DateTimeFormatter))
 
 (defrecord MySQLDriver []
@@ -148,6 +149,10 @@
       ;; statement param
       date)))
 
+(defmethod sqlqp/->honeysql [MySQLDriver Time]
+  [_ time-value]
+  (hx/->time time-value))
+
 ;; Since MySQL doesn't have date_trunc() we fake it by formatting a date to an appropriate string and then converting
 ;; back to a date. See http://dev.mysql.com/doc/refman/5.6/en/date-and-time-functions.html#function_date-format for an
 ;; explanation of format specifiers
diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj
index 99f3c77b472a4a1f0752478bfcd551178dd4b195..2a04a0490f06f847442c769a5fcb197d94fbbbbb 100644
--- a/src/metabase/driver/postgres.clj
+++ b/src/metabase/driver/postgres.clj
@@ -17,6 +17,7 @@
              [honeysql-extensions :as hx]
              [ssh :as ssh]])
   (:import java.util.UUID
+           java.sql.Time
            metabase.query_processor.interface.Value))
 
 (defrecord PostgresDriver []
@@ -199,6 +200,10 @@
       (isa? base-type :type/PostgresEnum) (hx/quoted-cast database-type value)
       :else                               (sqlqp/->honeysql driver value))))
 
+(defmethod sqlqp/->honeysql [PostgresDriver Time]
+  [driver time-value]
+  (hx/->time time-value))
+
 (defn- string-length-fn [field-key]
   (hsql/call :char_length (hx/cast :VARCHAR field-key)))
 
diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj
index 0da380793b89beb6095563beff4be86a0e7d8f7c..602738adf283a600ff0ab124dc8fd1cd2b03f153 100644
--- a/src/metabase/driver/presto.clj
+++ b/src/metabase/driver/presto.clj
@@ -1,6 +1,7 @@
 (ns metabase.driver.presto
   (:require [clj-http.client :as http]
             [clj-time
+             [coerce :as tcoerce]
              [core :as time]
              [format :as tformat]]
             [clojure
@@ -20,13 +21,14 @@
             [metabase.util
              [honeysql-extensions :as hx]
              [ssh :as ssh]])
-  (:import java.util.Date))
+  (:import java.sql.Time
+           java.util.Date
+           [metabase.query_processor.interface TimeValue]))
 
 (defrecord PrestoDriver []
   clojure.lang.Named
   (getName [_] "Presto"))
 
-
 ;;; Presto API helpers
 
 (defn- details->uri
@@ -56,10 +58,20 @@
 (def ^:private presto-date-time-formatter
   (u/->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS"))
 
+(defn- parse-presto-time
+  "Parsing time from presto using a specific formatter rather than the
+  utility functions as this will be called on each row returned, so
+  performance is important"
+  [time-str]
+  (->> time-str
+       (u/parse-date :hour-minute-second-ms)
+       tcoerce/to-long
+       Time.))
+
 (defn- field-type->parser [report-timezone field-type]
   (condp re-matches field-type
     #"decimal.*"                bigdec
-    #"time"                     (partial u/parse-date :hour-minute-second-ms)
+    #"time"                     parse-presto-time
     #"time with time zone"      parse-time-with-tz
     #"timestamp"                (partial u/parse-date
                                          (if-let [report-tz (and report-timezone
@@ -90,8 +102,9 @@
 
 (defn- execute-presto-query! [details query]
   (ssh/with-ssh-tunnel [details-with-tunnel details]
-    (let [{{:keys [columns data nextUri error]} :body} (http/post (details->uri details-with-tunnel "/v1/statement")
-                                                                  (assoc (details->request details-with-tunnel) :body query, :as :json))]
+    (let [{{:keys [columns data nextUri error]} :body :as foo} (http/post (details->uri details-with-tunnel "/v1/statement")
+                                                                          (assoc (details->request details-with-tunnel) :body query, :as :json))]
+
       (when error
         (throw (ex-info (or (:message error) "Error preparing query.") error)))
       (let [rows    (parse-presto-results (:report-timezone details) (or columns []) (or data []))
@@ -156,7 +169,8 @@
     #"varbinary.*" :type/*
     #"json"        :type/Text       ; TODO - this should probably be Dictionary or something
     #"date"        :type/Date
-    #"time.*"      :type/DateTime
+    #"time"        :type/Time
+    #"time.+"      :type/DateTime
     #"array"       :type/Array
     #"map"         :type/Dictionary
     #"row.*"       :type/*          ; TODO - again, but this time we supposedly have a schema
@@ -184,9 +198,27 @@
   [_ date]
   (hsql/call :from_iso8601_timestamp (hx/literal (u/date->iso-8601 date))))
 
+(def ^:private time-format (tformat/formatter "HH:mm:SS.SSS"))
+
+(defn- time->str
+  ([t]
+   (time->str t nil))
+  ([t tz-id]
+   (let [tz (time/time-zone-for-id tz-id)]
+     (tformat/unparse (tformat/with-zone time-format tz) (tcoerce/to-date-time t)))))
+
+(defmethod sqlqp/->honeysql [PrestoDriver TimeValue]
+  [_ {:keys [value timezone-id]}]
+  (hx/cast :time (time->str value timezone-id)))
+
+(defmethod sqlqp/->honeysql [PrestoDriver Time]
+  [_ {:keys [value]}]
+  (hx/->time (time->str value)))
+
 (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))
+  (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                (for [[col name] (map vector columns (rename-duplicates (map :name columns)))]
diff --git a/src/metabase/driver/sqlite.clj b/src/metabase/driver/sqlite.clj
index c0ac0df1f6442c2d69d00f393b526810233f1b42..326550fd18a5f22a486677fc96fcbd791aaa9a11 100644
--- a/src/metabase/driver/sqlite.clj
+++ b/src/metabase/driver/sqlite.clj
@@ -1,5 +1,8 @@
 (ns metabase.driver.sqlite
-  (:require [clojure
+  (:require [clj-time
+             [coerce :as tcoerce]
+             [format :as tformat]]
+            [clojure
              [set :as set]
              [string :as s]]
             [honeysql
@@ -11,7 +14,8 @@
              [util :as u]]
             [metabase.driver.generic-sql :as sql]
             [metabase.driver.generic-sql.query-processor :as sqlqp]
-            [metabase.util.honeysql-extensions :as hx]))
+            [metabase.util.honeysql-extensions :as hx])
+  (:import [java.sql Time Timestamp]))
 
 (defrecord SQLiteDriver []
   clojure.lang.Named
@@ -45,7 +49,8 @@
    [#"DECIMAL"  :type/Decimal]
    [#"BOOLEAN"  :type/Boolean]
    [#"DATETIME" :type/DateTime]
-   [#"DATE"     :type/Date]])
+   [#"DATE"     :type/Date]
+   [#"TIME"     :type/Time]])
 
 ;; register the SQLite concatnation operator `||` with HoneySQL as `sqlite-concat`
 ;; (hsql/format (hsql/call :sqlite-concat :a :b)) -> "(a || b)"
@@ -63,7 +68,7 @@
    See also the [SQLite Date and Time Functions Reference](http://www.sqlite.org/lang_datefunc.html)."
   [unit expr]
   ;; Convert Timestamps to ISO 8601 strings before passing to SQLite, otherwise they don't seem to work correctly
-  (let [v (if (instance? java.sql.Timestamp expr)
+  (let [v (if (instance? Timestamp expr)
             (hx/literal (u/date->iso-8601 expr))
             expr)]
     (case unit
@@ -153,6 +158,13 @@
   [_ bool]
   (if bool 1 0))
 
+(defmethod sqlqp/->honeysql [SQLiteDriver Time]
+  [_ time-value]
+  (->> time-value
+       tcoerce/to-date-time
+       (tformat/unparse (tformat/formatters :hour-minute-second-ms))
+       (hsql/call :time)))
+
 (defn- string-length-fn [field-key]
   (hsql/call :length field-key))
 
diff --git a/src/metabase/driver/sqlserver.clj b/src/metabase/driver/sqlserver.clj
index 0770e2c00396ef6f1ef4aa935f9a08effdede920..f2db2f24d5d7a4807a830210a0b2357c4139087e 100644
--- a/src/metabase/driver/sqlserver.clj
+++ b/src/metabase/driver/sqlserver.clj
@@ -9,7 +9,8 @@
             [metabase.driver.generic-sql.query-processor :as sqlqp]
             [metabase.util
              [honeysql-extensions :as hx]
-             [ssh :as ssh]]))
+             [ssh :as ssh]])
+  (:import java.sql.Time))
 
 (defrecord SQLServerDriver []
   clojure.lang.Named
@@ -155,6 +156,10 @@
   [_ bool]
   (if bool 1 0))
 
+(defmethod sqlqp/->honeysql [SQLServerDriver Time]
+  [_ time-value]
+  (hx/->time time-value))
+
 (defn- string-length-fn [field-key]
   (hsql/call :len (hx/cast :VARCHAR field-key)))
 
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 258b543b1bbcd46c8252ea584484470e61ff12c2..11ff649255ddca58f9a9561eea8ea603d70375b2 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -87,7 +87,7 @@
 
 (defn send-message!
   "Send an email to one or more RECIPIENTS.
-   RECIPIENTS is a sequence of email addresses; MESSAGE-TYPE must be either `:text` or `:html` or `:attachments`.
+  RECIPIENTS is a sequence of email addresses; MESSAGE-TYPE must be either `:text` or `:html` or `:attachments`.
 
      (email/send-message!
        :subject      \"[Metabase] Password Reset Request\"
@@ -95,7 +95,7 @@
        :message-type :text
        :message      \"How are you today?\")
 
-   Upon success, this returns the MESSAGE that was just sent. This function will catch and log any exception,
+  Upon success, this returns the MESSAGE that was just sent. This function will catch and log any exception,
   returning a map with a description of the error"
   {:style/indent 0}
   [& {:keys [subject recipients message-type message] :as msg-args}]
@@ -134,10 +134,9 @@
 (def ^:private email-security-order ["tls" "starttls" "ssl"])
 
 (defn- guess-smtp-security
-  "Attempts to use each of the security methods in security order with the same set of credentials.
-   This is used only when the initial connection attempt fails, so it won't overwrite a functioning
-   configuration. If this uses something other than the provided method, a warning gets printed on
-   the config page"
+  "Attempts to use each of the security methods in security order with the same set of credentials. This is used only
+  when the initial connection attempt fails, so it won't overwrite a functioning configuration. If this uses something
+  other than the provided method, a warning gets printed on the config page"
   [details]
   (loop [[security-type & more-to-try] email-security-order] ;; make sure this is not lazy, or chunking
     (when security-type                                      ;; can cause some servers to block requests
diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj
index 3dcaa7e71afe91aa703c84bd88c60015d9042687..22a2cc00592bb0cf6484c4c20cfc6346d917bab9 100644
--- a/src/metabase/email/messages.clj
+++ b/src/metabase/email/messages.clj
@@ -12,6 +12,7 @@
              [util :as u]]
             [metabase.pulse.render :as render]
             [metabase.util
+             [export :as export]
              [quotation :as quotation]
              [urls :as url]]
             [stencil
@@ -187,24 +188,57 @@
 (defn- pulse-context [pulse]
   (merge {:emailType    "pulse"
           :pulseName    (:name pulse)
-          :sectionStyle render/section-style
+          :sectionStyle (render/style render/section-style)
           :colorGrey4   render/color-gray-4
           :logoFooter   true}
          (random-quote-context)))
 
+(defn- create-temp-file
+  [suffix]
+  (doto (java.io.File/createTempFile "metabase_attachment" suffix)
+    .deleteOnExit))
+
+(defn- create-result-attachment-map [export-type card-name ^File attachment-file]
+  (let [{:keys [content-type ext]} (get export/export-formats export-type)]
+    {:type         :attachment
+     :content-type content-type
+     :file-name    (format "%s.%s" card-name ext)
+     :content      (-> attachment-file .toURI .toURL)
+     :description  (format "Full results for '%s'" card-name)}))
+
+(defn- result-attachments [results]
+  (remove nil?
+          (apply concat
+                 (for [{{card-name :name, csv? :include_csv, xls? :include_xls} :card :as result} results
+                       :when (and (or csv? xls?)
+                                  (seq (get-in result [:result :data :rows])))]
+                   [(when-let [temp-file (and csv? (create-temp-file "csv"))]
+                      (export/export-to-csv-writer temp-file result)
+                      (create-result-attachment-map "csv" card-name temp-file))
+
+                    (when-let [temp-file (and xls? (create-temp-file "xlsx"))]
+                      (export/export-to-xlsx-file temp-file result)
+                      (create-result-attachment-map "xlsx" card-name temp-file))]))))
+
 (defn- render-message-body [message-template message-context timezone results]
   (let [rendered-cards (binding [render/*include-title* true]
                          ;; doall to ensure we haven't exited the binding before the valures are created
                          (doall (map #(render/render-pulse-section timezone %) results)))
         message-body   (assoc message-context :pulse (html (vec (cons :div (map :content rendered-cards)))))
         attachments    (apply merge (map :attachments rendered-cards))]
-    (vec (cons {:type "text/html; charset=utf-8" :content (stencil/render-file message-template message-body)}
-               (map make-message-attachment attachments)))))
+    (vec (concat [{:type "text/html; charset=utf-8" :content (stencil/render-file message-template message-body)}]
+                 (map make-message-attachment attachments)
+                 (result-attachments results)))))
+
+(defn- assoc-attachment-booleans [pulse results]
+  (for [{{result-card-id :id} :card :as result} results
+        :let [pulse-card (m/find-first #(= (:id %) result-card-id) (:cards pulse))]]
+    (update result :card merge (select-keys pulse-card [:include_csv :include_xls]))))
 
 (defn render-pulse-email
   "Take a pulse object and list of results, returns an array of attachment objects for an email"
   [timezone pulse results]
-  (render-message-body "metabase/email/pulse" (pulse-context pulse) timezone results))
+  (render-message-body "metabase/email/pulse" (pulse-context pulse) timezone (assoc-attachment-booleans pulse results)))
 
 (defn pulse->alert-condition-kwd
   "Given an `ALERT` return a keyword representing what kind of goal needs to be met."
@@ -248,7 +282,8 @@
   (let [message-ctx  (default-alert-context alert (alert-results-condition-text goal-value))]
     (render-message-body "metabase/email/alert"
                          (assoc message-ctx :firstRunOnly? alert_first_only)
-                         timezone results)))
+                         timezone
+                         (assoc-attachment-booleans alert results))))
 
 (def ^:private alert-condition-text
   {:meets "when this question meets its goal"
diff --git a/src/metabase/events/driver_notifications.clj b/src/metabase/events/driver_notifications.clj
index 734411b4e3a8e9a79c0633565486d7894e5e233f..87af2bedd9423b2932f2e3627079f372c765962e 100644
--- a/src/metabase/events/driver_notifications.clj
+++ b/src/metabase/events/driver_notifications.clj
@@ -1,4 +1,9 @@
 (ns metabase.events.driver-notifications
+  "Driver notifications are used to let drivers know database details or other relevant information has
+  changed (`:database-update`) or that a Database has been deleted (`:database-delete`). Drivers can choose to be
+  notified of these events by implementing the `notify-database-updated` method of `IDriver`. At the time of this
+  writing, the Generic SQL driver 'superclass' is the only thing that implements this method, and does so to close
+  connection pools when database details change or when they are deleted."
   (:require [clojure.core.async :as async]
             [clojure.tools.logging :as log]
             [metabase
@@ -14,7 +19,7 @@
   (async/chan))
 
 
-;;; ## ---------------------------------------- EVENT PROCESSING ----------------------------------------
+;;; ------------------------------------------------ EVENT PROCESSING ------------------------------------------------
 
 
 (defn process-driver-notifications-event
@@ -30,7 +35,7 @@
 
 
 
-;;; ## ---------------------------------------- LIFECYLE ----------------------------------------
+;;; ---------------------------------------------------- LIFECYLE ----------------------------------------------------
 
 
 (defn events-init
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index b4ea7aaa0ba758a2da76b19cbde43f6a5e16d1b2..51e8257c0566830e976ce88754a8b23e19904db5 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -11,7 +11,8 @@
              [db :as mdb]
              [public-settings :as public-settings]
              [util :as u]]
-            [metabase.api.common :refer [*current-user* *current-user-id* *current-user-permissions-set* *is-superuser?*]]
+            [metabase.api.common :refer [*current-user* *current-user-id* *current-user-permissions-set*
+                                         *is-superuser?*]]
             [metabase.api.common.internal :refer [*automatically-catch-api-exceptions*]]
             [metabase.core.initialization-status :as init-status]
             [metabase.models
@@ -27,7 +28,7 @@
   (:import com.fasterxml.jackson.core.JsonGenerator
            java.io.OutputStream))
 
-;;; # ------------------------------------------------------------ UTIL FNS ------------------------------------------------------------
+;;; ---------------------------------------------------- UTIL FNS ----------------------------------------------------
 
 (defn- api-call?
   "Is this ring request an API call (does path start with `/api`)?"
@@ -54,7 +55,7 @@
   [{:keys [uri]}]
   (re-matches #"^/embed/.*$" uri))
 
-;;; # ------------------------------------------------------------ AUTH & SESSION MANAGEMENT ------------------------------------------------------------
+;;; ------------------------------------------- AUTH & SESSION MANAGEMENT --------------------------------------------
 
 (def ^:private ^:const ^String metabase-session-cookie "metabase.SESSION_ID")
 (def ^:private ^:const ^String metabase-session-header "x-metabase-session")
@@ -126,12 +127,13 @@
   (vec (concat [User :is_active :google_auth :ldap_auth] (models/default-fields User))))
 
 (defn bind-current-user
-  "Middleware that binds `metabase.api.common/*current-user*`, `*current-user-id*`, `*is-superuser?*`, and `*current-user-permissions-set*`.
+  "Middleware that binds `metabase.api.common/*current-user*`, `*current-user-id*`, `*is-superuser?*`, and
+  `*current-user-permissions-set*`.
 
-   *  `*current-user-id*`             int ID or nil of user associated with request
-   *  `*current-user*`                delay that returns current user (or nil) from DB
-   *  `*is-superuser?*`               Boolean stating whether current user is a superuser.
-   *  `current-user-permissions-set*` delay that returns the set of permissions granted to the current user from DB"
+  *  `*current-user-id*`             int ID or nil of user associated with request
+  *  `*current-user*`                delay that returns current user (or nil) from DB
+  *  `*is-superuser?*`               Boolean stating whether current user is a superuser.
+  *  `current-user-permissions-set*` delay that returns the set of permissions granted to the current user from DB"
   [handler]
   (fn [request]
     (if-let [current-user-id (:metabase-user-id request)]
@@ -144,8 +146,8 @@
 
 
 (defn wrap-api-key
-  "Middleware that sets the `:metabase-api-key` keyword on the request if a valid API Key can be found.
-   We check the request headers for `X-METABASE-APIKEY` and if it's not found then then no keyword is bound to the request."
+  "Middleware that sets the `:metabase-api-key` keyword on the request if a valid API Key can be found. We check the
+  request headers for `X-METABASE-APIKEY` and if it's not found then then no keyword is bound to the request."
   [handler]
   (comp handler (fn [{:keys [headers] :as request}]
                   (if-let [api-key (headers metabase-api-key-header)]
@@ -156,11 +158,11 @@
 (defn enforce-api-key
   "Middleware that enforces validation of the client via API Key, cancelling the request processing if the check fails.
 
-   Validation is handled by first checking for the presence of the `:metabase-api-key` on the request.  If the api key
-   is available then we validate it by checking it against the configured `:mb-api-key` value set in our global config.
+  Validation is handled by first checking for the presence of the `:metabase-api-key` on the request.  If the api key
+  is available then we validate it by checking it against the configured `:mb-api-key` value set in our global config.
 
-   If the request `:metabase-api-key` matches the configured `:mb-api-key` value then the request continues, otherwise we
-   reject the request and return a 403 Forbidden response."
+  If the request `:metabase-api-key` matches the configured `:mb-api-key` value then the request continues, otherwise
+  we reject the request and return a 403 Forbidden response."
   [handler]
   (fn [{:keys [metabase-api-key] :as request}]
     (if (= (config/config-str :mb-api-key) metabase-api-key)
@@ -169,7 +171,7 @@
       response-forbidden)))
 
 
-;;; # ------------------------------------------------------------ SECURITY HEADERS ------------------------------------------------------------
+;;; ------------------------------------------------ security HEADERS ------------------------------------------------
 
 (defn- cache-prevention-headers
   "Headers that tell browsers not to cache a response."
@@ -179,44 +181,47 @@
    "Last-Modified"  (u/format-date :rfc822)})
 
 (def ^:private ^:const strict-transport-security-header
-  "Tell browsers to only access this resource over HTTPS for the next year (prevent MTM attacks).
-   (This only applies if the original request was HTTPS; if sent in response to an HTTP request, this is simply ignored)"
+  "Tell browsers to only access this resource over HTTPS for the next year (prevent MTM attacks). (This only applies if
+  the original request was HTTPS; if sent in response to an HTTP request, this is simply ignored)"
   {"Strict-Transport-Security" "max-age=31536000"})
 
 (def ^:private ^:const content-security-policy-header
-  "`Content-Security-Policy` header. See [http://content-security-policy.com](http://content-security-policy.com) for more details."
-  {"Content-Security-Policy" (apply str (for [[k vs] {:default-src ["'none'"]
-                                                      :script-src  ["'unsafe-inline'"
-                                                                    "'unsafe-eval'"
-                                                                    "'self'"
-                                                                    "https://maps.google.com"
-                                                                    "https://apis.google.com"
-                                                                    "https://www.google-analytics.com" ; Safari requires the protocol
-                                                                    "https://*.googleapis.com"
-                                                                    "*.gstatic.com"
-                                                                    (when config/is-dev?
-                                                                      "localhost:8080")]
-                                                      :child-src   ["'self'"
-                                                                    "https://accounts.google.com"] ; TODO - double check that we actually need this for Google Auth
-                                                      :style-src   ["'unsafe-inline'"
-                                                                    "'self'"
-                                                                    "fonts.googleapis.com"]
-                                                      :font-src    ["'self'"
-                                                                    "fonts.gstatic.com"
-                                                                    "themes.googleusercontent.com"
-                                                                    (when config/is-dev?
-                                                                      "localhost:8080")]
-                                                      :img-src     ["*"
-                                                                    "'self' data:"]
-                                                      :connect-src ["'self'"
-                                                                    "metabase.us10.list-manage.com"
-                                                                    (when config/is-dev?
-                                                                      "localhost:8080 ws://localhost:8080")]}]
-                                          (format "%s %s; " (name k) (apply str (interpose " " vs)))))})
+  "`Content-Security-Policy` header. See https://content-security-policy.com for more details."
+  {"Content-Security-Policy"
+   (apply str (for [[k vs] {:default-src ["'none'"]
+                            :script-src  ["'unsafe-inline'"
+                                          "'unsafe-eval'"
+                                          "'self'"
+                                          "https://maps.google.com"
+                                          "https://apis.google.com"
+                                          "https://www.google-analytics.com" ; Safari requires the protocol
+                                          "https://*.googleapis.com"
+                                          "*.gstatic.com"
+                                          (when config/is-dev?
+                                            "localhost:8080")]
+                            :child-src   ["'self'"
+                                          ;; TODO - double check that we actually need this for Google Auth
+                                          "https://accounts.google.com"]
+                            :style-src   ["'unsafe-inline'"
+                                          "'self'"
+                                          "fonts.googleapis.com"]
+                            :font-src    ["'self'"
+                                          "fonts.gstatic.com"
+                                          "themes.googleusercontent.com"
+                                          (when config/is-dev?
+                                            "localhost:8080")]
+                            :img-src     ["*"
+                                          "'self' data:"]
+                            :connect-src ["'self'"
+                                          "metabase.us10.list-manage.com"
+                                          (when config/is-dev?
+                                            "localhost:8080 ws://localhost:8080")]}]
+                (format "%s %s; " (name k) (apply str (interpose " " vs)))))})
 
 (defsetting ssl-certificate-public-key
   "Base-64 encoded public key for this site's SSL certificate. Specify this to enable HTTP Public Key Pinning.
-   See http://mzl.la/1EnfqBf for more information.") ; TODO - it would be nice if we could make this a proper link in the UI; consider enabling markdown parsing
+   See http://mzl.la/1EnfqBf for more information.")
+;; TODO - it would be nice if we could make this a proper link in the UI; consider enabling markdown parsing
 
 #_(defn- public-key-pins-header []
   (when-let [k (ssl-certificate-public-key)]
@@ -228,15 +233,20 @@
          #_(public-key-pins-header)))
 
 (defn- html-page-security-headers [& {:keys [allow-iframes?] }]
-  (merge (cache-prevention-headers)
-         strict-transport-security-header
-         content-security-policy-header
-         #_(public-key-pins-header)
-         (when-not allow-iframes?
-           {"X-Frame-Options"                 "DENY"})        ; Tell browsers not to render our site as an iframe (prevent clickjacking)
-         {"X-XSS-Protection"                  "1; mode=block" ; Tell browser to block suspected XSS attacks
-          "X-Permitted-Cross-Domain-Policies" "none"          ; Prevent Flash / PDF files from including content from site.
-          "X-Content-Type-Options"            "nosniff"}))    ; Tell browser not to use MIME sniffing to guess types of files -- protect against MIME type confusion attacks
+  (merge
+   (cache-prevention-headers)
+   strict-transport-security-header
+   content-security-policy-header
+   #_(public-key-pins-header)
+   (when-not allow-iframes?
+     ;; Tell browsers not to render our site as an iframe (prevent clickjacking)
+     {"X-Frame-Options"                 "DENY"})
+   { ;; Tell browser to block suspected XSS attacks
+    "X-XSS-Protection"                  "1; mode=block"
+    ;; Prevent Flash / PDF files from including content from site.
+    "X-Permitted-Cross-Domain-Policies" "none"
+    ;; Tell browser not to use MIME sniffing to guess types of files -- protect against MIME type confusion attacks
+    "X-Content-Type-Options"            "nosniff"}))
 
 (defn add-security-headers
   "Add HTTP headers to tell browsers not to cache API responses."
@@ -250,11 +260,12 @@
                                         (index? request)    (html-page-security-headers))))))
 
 
-;;; # ------------------------------------------------------------ SETTING SITE-URL ------------------------------------------------------------
+;;; ------------------------------------------------ SETTING SITE-URL ------------------------------------------------
 
-;; It's important for us to know what the site URL is for things like returning links, etc.
-;; this is stored in the `site-url` Setting; we can set it automatically by looking at the `Origin` or `Host` headers sent with a request.
-;; Effectively the very first API request that gets sent to us (usually some sort of setup request) ends up setting the (initial) value of `site-url`
+;; It's important for us to know what the site URL is for things like returning links, etc. this is stored in the
+;; `site-url` Setting; we can set it automatically by looking at the `Origin` or `Host` headers sent with a request.
+;; Effectively the very first API request that gets sent to us (usually some sort of setup request) ends up setting
+;; the (initial) value of `site-url`
 
 (defn maybe-set-site-url
   "Middleware to set the `site-url` Setting if it's unset the first time a request is made."
@@ -268,7 +279,7 @@
     (handler request)))
 
 
-;;; # ------------------------------------------------------------ JSON SERIALIZATION CONFIG ------------------------------------------------------------
+;;; ------------------------------------------- JSON SERIALIZATION CONFIG --------------------------------------------
 
 ;; Tell the JSON middleware to use a date format that includes milliseconds (why?)
 (def ^:private ^:const default-date-format "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
@@ -277,8 +288,8 @@
 
 ;; ## Custom JSON encoders
 
-;; Always fall back to `.toString` instead of barfing.
-;; In some cases we should be able to improve upon this behavior; `.toString` may just return the Class and address, e.g. `some.Class@72a8b25e`
+;; Always fall back to `.toString` instead of barfing. In some cases we should be able to improve upon this behavior;
+;; `.toString` may just return the Class and address, e.g. `some.Class@72a8b25e`
 ;; The following are known few classes where `.toString` is the optimal behavior:
 ;; *  `org.postgresql.jdbc4.Jdbc4Array` (Postgres arrays)
 ;; *  `org.bson.types.ObjectId`         (Mongo BSON IDs)
@@ -300,7 +311,7 @@
                                     (.writeString json-generator ^String (apply str "0x" (for [b (take 4 byte-ar)]
                                                                                            (format "%02X" b))))))
 
-;;; # ------------------------------------------------------------ LOGGING ------------------------------------------------------------
+;;; ---------------------------------------------------- LOGGING -----------------------------------------------------
 
 (defn- log-response [{:keys [uri request-method]} {:keys [status body]} elapsed-time db-call-count]
   (let [log-error #(log/error %) ; these are macros so we can't pass by value :sad:
@@ -311,7 +322,8 @@
                                 (=  status 403) [true  'red   log-warn]
                                 (>= status 400) [true  'red   log-debug]
                                 :else           [false 'green log-debug])]
-    (log-fn (str (u/format-color color "%s %s %d (%s) (%d DB calls)" (.toUpperCase (name request-method)) uri status elapsed-time db-call-count)
+    (log-fn (str (u/format-color color "%s %s %d (%s) (%d DB calls)"
+                   (.toUpperCase (name request-method)) uri status elapsed-time db-call-count)
                  ;; only print body on error so we don't pollute our environment by over-logging
                  (when (and error?
                             (or (string? body) (coll? body)))
@@ -331,11 +343,11 @@
             (log-response request <> (u/format-nanoseconds (- (System/nanoTime) start-time)) (call-count))))))))
 
 
-;;; ------------------------------------------------------------ EXCEPTION HANDLING ------------------------------------------------------------
+;;; ----------------------------------------------- EXCEPTION HANDLING -----------------------------------------------
 
 (defn genericize-exceptions
-  "Catch any exceptions thrown in the request handler body and rethrow a generic 400 exception instead.
-   This minimizes information available to bad actors when exceptions occur on public endpoints."
+  "Catch any exceptions thrown in the request handler body and rethrow a generic 400 exception instead. This minimizes
+  information available to bad actors when exceptions occur on public endpoints."
   [handler]
   (fn [request]
     (try (binding [*automatically-catch-api-exceptions* false]
@@ -345,10 +357,9 @@
            {:status 400, :body "An error occurred."}))))
 
 (defn message-only-exceptions
-  "Catch any exceptions thrown in the request handler body and rethrow a 400 exception that only has
-   the message from the original instead (i.e., don't rethrow the original stacktrace).
-   This reduces the information available to bad actors but still provides some information that will
-   prove useful in debugging errors."
+  "Catch any exceptions thrown in the request handler body and rethrow a 400 exception that only has the message from
+  the original instead (i.e., don't rethrow the original stacktrace). This reduces the information available to bad
+  actors but still provides some information that will prove useful in debugging errors."
   [handler]
   (fn [request]
     (try (binding [*automatically-catch-api-exceptions* false]
@@ -356,11 +367,12 @@
          (catch Throwable e
            {:status 400, :body (.getMessage e)}))))
 
-;;; ------------------------------------------------------------ EXCEPTION HANDLING ------------------------------------------------------------
+
+;;; --------------------------------------------------- STREAMING ----------------------------------------------------
 
 (def ^:private ^:const streaming-response-keep-alive-interval-ms
-  "Interval between sending newline characters to keep Heroku from terminating
-   requests like queries that take a long time to complete."
+  "Interval between sending newline characters to keep Heroku from terminating requests like queries that take a long
+  time to complete."
   (* 1 1000))
 
 ;; Handle ring response maps that contain a core.async chan in the :body key:
@@ -388,13 +400,11 @@
           (recur (async/<!! output-queue)))))))
 
 (defn streaming-json-response
-  "This midelware assumes handlers fail early or return success
-   Run the handler in a future and send newlines to keep the connection open
-   and help detect when the browser is no longer listening for the response.
-   Waits for one second to see if the handler responds immediately, If it does
-   then there is no need to stream the response and it is sent back directly.
-   In cases where it takes longer than a second, assume the eventual result will
-   be a success and start sending newlines to keep the connection open."
+  "This midelware assumes handlers fail early or return success Run the handler in a future and send newlines to keep
+  the connection open and help detect when the browser is no longer listening for the response. Waits for one second
+  to see if the handler responds immediately, If it does then there is no need to stream the response and it is sent
+  back directly. In cases where it takes longer than a second, assume the eventual result will be a success and start
+  sending newlines to keep the connection open."
   [handler]
   (fn [request]
     (let [response            (future (handler request))
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index e4be989e30bdf916ff0c0a722ed100fd672dcbb5..dbe47eec12ab2da9d1c567282e568e824c1ce8c9 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -6,7 +6,6 @@
             [clojure.tools.logging :as log]
             [metabase
              [public-settings :as public-settings]
-             [query :as q]
              [query-processor :as qp]
              [util :as u]]
             [metabase.api.common :as api :refer [*current-user-id*]]
@@ -22,6 +21,7 @@
              [revision :as revision]]
             [metabase.query-processor.middleware.permissions :as qp-perms]
             [metabase.query-processor.util :as qputil]
+            [metabase.util.query :as q]
             [toucan
              [db :as db]
              [models :as models]]))
@@ -64,7 +64,8 @@
                                          (map :id)
                                          set))]
     (for [card cards]
-      (assoc card :in_public_dashboard (contains? public-dashboard-card-ids (u/get-id card))))))
+      (when (some? card) ; card may be `nil` here if it comes from a text-only Dashcard
+        (assoc card :in_public_dashboard (contains? public-dashboard-card-ids (u/get-id card)))))))
 
 
 ;;; ---------------------------------------------- Permissions Checking ----------------------------------------------
diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj
index 0af15018fa4b32acffa7208a92f3020e22bf143a..7db45ffd528ffdf8f01eca7cafe0200129a89930 100644
--- a/src/metabase/models/collection.clj
+++ b/src/metabase/models/collection.clj
@@ -58,18 +58,22 @@
   (if-not collection-name
     collection
     (assoc collection :slug (u/prog1 (slugify collection-name)
-                              (or (db/exists? Collection, :slug <>, :id id) ; if slug hasn't changed no need to check for uniqueness
-                                  (assert-unique-slug <>))))))              ; otherwise check to make sure the new slug is unique
+                              ;; if slug hasn't changed no need to check for uniqueness otherwise check to make sure
+                              ;; the new slug is unique
+                              (or (db/exists? Collection, :slug <>, :id id)
+                                  (assert-unique-slug <>))))))
 
 (defn- pre-delete [collection]
-  ;; unset the collection_id for Cards in this collection. This is mostly for the sake of tests since IRL we shouldn't be deleting collections, but rather archiving them instead
+  ;; unset the collection_id for Cards in this collection. This is mostly for the sake of tests since IRL we shouldn't
+  ;; be deleting collections, but rather archiving them instead
   (db/update-where! 'Card {:collection_id (u/get-id collection)}
     :collection_id nil))
 
 (defn perms-objects-set
   "Return the required set of permissions to READ-OR-WRITE COLLECTION-OR-ID."
   [collection-or-id read-or-write]
-  ;; This is not entirely accurate as you need to be a superuser to modifiy a collection itself (e.g., changing its name) but if you have write perms you can add/remove cards
+  ;; This is not entirely accurate as you need to be a superuser to modifiy a collection itself (e.g., changing its
+  ;; name) but if you have write perms you can add/remove cards
   #{(case read-or-write
       :read  (perms/collection-read-path collection-or-id)
       :write (perms/collection-readwrite-path collection-or-id))})
@@ -90,11 +94,11 @@
           :perms-objects-set perms-objects-set}))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------------------------------------------------------+
-;;; |                                                                       PERMISSIONS GRAPH                                                                        |
-;;; +----------------------------------------------------------------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                               PERMISSIONS GRAPH                                                |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
-;;; ---------------------------------------- Schemas ----------------------------------------
+;;; ---------------------------------------------------- Schemas -----------------------------------------------------
 
 (def ^:private CollectionPermissions
   (s/enum :write :read :none))
@@ -108,7 +112,7 @@
    :groups   {su/IntGreaterThanZero GroupPermissionsGraph}})
 
 
-;;; ---------------------------------------- Fetch Graph ----------------------------------------
+;;; -------------------------------------------------- Fetch Graph ---------------------------------------------------
 
 (defn- group-id->permissions-set []
   (into {} (for [[group-id perms] (group-by :group_id (db/select 'Permissions))]
@@ -128,8 +132,9 @@
              {collection-id (perms-type-for-collection permissions-set collection-id)})))
 
 (s/defn graph :- PermissionsGraph
-  "Fetch a graph representing the current permissions status for every group and all permissioned collections.
-   This works just like the function of the same name in `metabase.models.permissions`; see also the documentation for that function."
+  "Fetch a graph representing the current permissions status for every group and all permissioned collections. This
+  works just like the function of the same name in `metabase.models.permissions`; see also the documentation for that
+  function."
   []
   (let [group-id->perms (group-id->permissions-set)
         collection-ids  (db/select-ids 'Collection)]
@@ -138,9 +143,10 @@
                           {group-id (group-permissions-graph (group-id->perms group-id) collection-ids)}))}))
 
 
-;;; ---------------------------------------- Update Graph ----------------------------------------
+;;; -------------------------------------------------- Update Graph --------------------------------------------------
 
-(s/defn ^:private update-collection-permissions! [group-id :- su/IntGreaterThanZero, collection-id :- su/IntGreaterThanZero, new-collection-perms :- CollectionPermissions]
+(s/defn ^:private update-collection-permissions!
+  [group-id :- su/IntGreaterThanZero, collection-id :- su/IntGreaterThanZero, new-collection-perms :- CollectionPermissions]
   ;; remove whatever entry is already there (if any) and add a new entry if applicable
   (perms/revoke-collection-permissions! group-id collection-id)
   (case new-collection-perms
@@ -157,16 +163,18 @@
    This doesn't do anything if `*current-user-id*` is unset (e.g. for testing or REPL usage)."
   [current-revision old new]
   (when *current-user-id*
+    ;; manually specify ID here so if one was somehow inserted in the meantime in the fraction of a second since we
+    ;; called `check-revision-numbers` the PK constraint will fail and the transaction will abort
     (db/insert! CollectionRevision
-      :id     (inc current-revision) ; manually specify ID here so if one was somehow inserted in the meantime in the fraction of a second
-      :before  old                   ; since we called `check-revision-numbers` the PK constraint will fail and the transaction will abort
+      :id     (inc current-revision)
+      :before  old
       :after   new
       :user_id *current-user-id*)))
 
 (s/defn update-graph!
-  "Update the collections permissions graph.
-   This works just like the function of the same name in `metabase.models.permissions`, but for `Collections`;
-   refer to that function's extensive documentation to get a sense for how this works."
+  "Update the collections permissions graph. This works just like the function of the same name in
+  `metabase.models.permissions`, but for `Collections`; refer to that function's extensive documentation to get a
+  sense for how this works."
   ([new-graph :- PermissionsGraph]
    (let [old-graph (graph)
          [old new] (data/diff (:groups old-graph) (:groups new-graph))]
diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj
index cc6e0936842bfc5598a6f87bd5bcb5bf3f9b12bf..c3ad7a139c19b54e4250d864aca97865b45e73dc 100644
--- a/src/metabase/models/dashboard.clj
+++ b/src/metabase/models/dashboard.clj
@@ -36,7 +36,9 @@
         (some? public-uuid))
    ;; if Dashboard is already hydrated no need to do it a second time
    (let [cards (or (dashcards->cards (:ordered_cards dashboard))
-                   (dashcards->cards (-> (db/select [DashboardCard :id :card_id], :dashboard_id (u/get-id dashboard))
+                   (dashcards->cards (-> (db/select [DashboardCard :id :card_id]
+                                           :dashboard_id (u/get-id dashboard)
+                                           :card_id      [:not= nil]) ; skip text-only Cards
                                          (hydrate [:card :in_public_dashboard] :series))))]
      (or (empty? cards)
          (some i/can-read? cards)))))
diff --git a/src/metabase/models/dashboard_card.clj b/src/metabase/models/dashboard_card.clj
index 57b6730c87010188afb899e181079c9379da63b3..daeefc7a5b4f3e821db5f0f77ce20dcd1b490c7d 100644
--- a/src/metabase/models/dashboard_card.clj
+++ b/src/metabase/models/dashboard_card.clj
@@ -68,7 +68,7 @@
 (defn ^:hydrate series
   "Return the `Cards` associated as additional series on this `DashboardCard`."
   [{:keys [id]}]
-  (db/select [Card :id :name :description :display :dataset_query :visualization_settings]
+  (db/select [Card :id :name :description :display :dataset_query :visualization_settings :collection_id]
     (mdb/join [Card :id] [DashboardCardSeries :card_id])
     (db/qualify DashboardCardSeries :dashboardcard_id) id
     {:order-by [[(db/qualify DashboardCardSeries :position) :asc]]}))
diff --git a/src/metabase/models/metric.clj b/src/metabase/models/metric.clj
index 81fb9cf81b1e22e5c2fb72d95affa7342437395c..dd9c6389f64088010784dd50cc906942663728fd 100644
--- a/src/metabase/models/metric.clj
+++ b/src/metabase/models/metric.clj
@@ -2,12 +2,12 @@
   (:require [medley.core :as m]
             [metabase
              [events :as events]
-             [query :as q]
              [util :as u]]
             [metabase.models
              [dependency :as dependency]
              [interface :as i]
              [revision :as revision]]
+            [metabase.util.query :as q]
             [toucan
              [db :as db]
              [hydrate :refer [hydrate]]
diff --git a/src/metabase/models/permissions_group.clj b/src/metabase/models/permissions_group.clj
index 34a9f0f635bb517e4325c6a5483e6c93f1002d7c..1dac6e94532e53a333489ec5836ee3e80f9f070f 100644
--- a/src/metabase/models/permissions_group.clj
+++ b/src/metabase/models/permissions_group.clj
@@ -1,5 +1,12 @@
 (ns metabase.models.permissions-group
-  (:require [clojure.string :as s]
+  "A `PermissionsGroup` is a group (or role) that can be assigned certain permissions. Users can be members of one or
+  more of these groups.
+
+  A few 'magic' groups exist: `all-users`, which predicably contains All Users; `admin`, which contains all
+  superusers, and `metabot`, which is used to set permissions for the MetaBot. These groups are 'magic' in the sense
+  that you cannot add users to them yourself, nor can you delete them; they are created automatically. You can,
+  however, set permissions for them. "
+  (:require [clojure.string :as str]
             [clojure.tools.logging :as log]
             [metabase.models.setting :as setting]
             [metabase.util :as u]
@@ -10,7 +17,7 @@
 (models/defmodel PermissionsGroup :permissions_group)
 
 
-;;; ------------------------------------------------------------ Magic Groups Getter Fns ------------------------------------------------------------
+;;; -------------------------------------------- Magic Groups Getter Fns ---------------------------------------------
 
 (defn- group-fetch-fn [group-name]
   (memoize (fn []
@@ -36,14 +43,14 @@
   (group-fetch-fn "MetaBot"))
 
 
-;;; ------------------------------------------------------------ Validation ------------------------------------------------------------
+;;; --------------------------------------------------- Validation ---------------------------------------------------
 
 (defn exists-with-name?
   "Does a `PermissionsGroup` with GROUP-NAME exist in the DB? (case-insensitive)"
   ^Boolean [group-name]
   {:pre [((some-fn keyword? string?) group-name)]}
   (db/exists? PermissionsGroup
-    :%lower.name (s/lower-case (name group-name))))
+    :%lower.name (str/lower-case (name group-name))))
 
 (defn- check-name-not-already-taken
   [group-name]
@@ -62,7 +69,7 @@
                {:status-code 400})))))
 
 
-;;; ------------------------------------------------------------ Lifecycle ------------------------------------------------------------
+;;; --------------------------------------------------- Lifecycle ----------------------------------------------------
 
 (defn- pre-insert [{group-name :name, :as group}]
   (u/prog1 group
@@ -92,7 +99,7 @@
                     :pre-update         pre-update}))
 
 
-;;; ------------------------------------------------------------ Util Fns ------------------------------------------------------------
+;;; ---------------------------------------------------- Util Fns ----------------------------------------------------
 
 
 (defn ^:hydrate members
diff --git a/src/metabase/models/pulse.clj b/src/metabase/models/pulse.clj
index a845295f7c884071da81369373e1098c60ba8f56..8854e5848bab88510760fb3471168b8e212879b8 100644
--- a/src/metabase/models/pulse.clj
+++ b/src/metabase/models/pulse.clj
@@ -78,12 +78,16 @@
 (defn ^:hydrate cards
   "Return the `Cards` associated with this PULSE."
   [{:keys [id]}]
-  (db/select [Card :id :name :description :display]
-    :archived false
-    (mdb/join [Card :id] [PulseCard :card_id])
-    (db/qualify PulseCard :pulse_id) id
-    {:order-by [[(db/qualify PulseCard :position) :asc]]}))
-
+  (map #(models/do-post-select Card %)
+       (db/query
+        {:select    [:c.id :c.name :c.description :c.display :pc.include_csv :pc.include_xls]
+         :from      [[Pulse :p]]
+         :join      [[PulseCard :pc] [:= :p.id :pc.pulse_id]
+                     [Card :c] [:= :c.id :pc.card_id]]
+         :where     [:and
+                     [:= :p.id id]
+                     [:= :c.archived false]]
+         :order-by [[:pc.position :asc]]})))
 
 ;;; ------------------------------------------------------------ Pulse Fetching Helper Fns ------------------------------------------------------------
 
@@ -179,6 +183,13 @@
                              [:not= :p.alert_condition nil]
                              [:in :pc.card_id card-ids]]}))))
 
+(defn create-card-ref
+  "Create a card reference from a card or id"
+  [card]
+  {:id          (u/get-id card)
+   :include_csv (get card :include_csv false)
+   :include_xls (get card :include_xls false)})
+
 ;;; ------------------------------------------------------------ Other Persistence Functions ------------------------------------------------------------
 
 (defn update-pulse-cards!
@@ -188,16 +199,20 @@
    *  If an ID in CARD-IDS has no corresponding existing `PulseCard` object, one will be created.
    *  If an existing `PulseCard` has no corresponding ID in CARD-IDs, it will be deleted.
    *  All cards will be updated with a `position` according to their place in the collection of CARD-IDS"
-  {:arglists '([pulse card-ids])}
-  [{:keys [id]} card-ids]
+  {:arglists '([pulse card-refs])}
+  [{:keys [id]} card-refs]
   {:pre [(integer? id)
-         (sequential? card-ids)
-         (every? integer? card-ids)]}
+         (sequential? card-refs)
+         (every? map? card-refs)]}
   ;; first off, just delete any cards associated with this pulse (we add them again below)
   (db/delete! PulseCard :pulse_id id)
   ;; now just insert all of the cards that were given to us
-  (when (seq card-ids)
-    (let [cards (map-indexed (fn [i card-id] {:pulse_id id, :card_id card-id, :position i}) card-ids)]
+  (when (seq card-refs)
+    (let [cards (map-indexed (fn [i {card-id :id :keys [include_csv include_xls]}]
+                               {:pulse_id    id, :card_id     card-id,
+                                :position    i   :include_csv include_csv,
+                                :include_xls include_xls})
+                             card-refs)]
       (db/insert-many! PulseCard cards))))
 
 
@@ -263,7 +278,7 @@
          (integer? creator-id)
          (sequential? card-ids)
          (seq card-ids)
-         (every? integer? card-ids)
+         (every? map? card-ids)
          (coll? channels)
          (every? map? channels)]}
   (let [id (create-notification {:creator_id    creator-id
@@ -305,7 +320,7 @@
          (string? name)
          (sequential? cards)
          (> (count cards) 0)
-         (every? integer? cards)
+         (every? map? cards)
          (coll? channels)
          (every? map? channels)]}
   (update-notification! pulse)
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index 488a39e7a29a85a2361c3ba499a510b8ad139b43..15ec1856fe0197f72d31b9e7514439450ccae8da 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -14,7 +14,7 @@
              [models :as models]])
   (:import java.util.UUID))
 
-;;; ------------------------------------------------------------ Entity & Lifecycle ------------------------------------------------------------
+;;; ----------------------------------------------- Entity & Lifecycle -----------------------------------------------
 
 (models/defmodel User :core_user)
 
@@ -108,7 +108,7 @@
           :pre-delete     pre-delete}))
 
 
-;; ------------------------------------------------------------ Helper Fns ------------------------------------------------------------
+;;; --------------------------------------------------- Helper Fns ---------------------------------------------------
 
 (declare form-password-reset-url set-password-reset-token!)
 
@@ -131,7 +131,8 @@
     (send-welcome-email! <> invitor)))
 
 (defn create-new-google-auth-user!
-  "Convenience for creating a new user via Google Auth. This account is considered active immediately; thus all active admins will recieve an email right away."
+  "Convenience for creating a new user via Google Auth. This account is considered active immediately; thus all active
+  admins will recieve an email right away."
   [first-name last-name email-address]
   {:pre [(string? first-name) (string? last-name) (u/is-email? email-address)]}
   (u/prog1 (db/insert! User
@@ -144,7 +145,8 @@
     (email/send-user-joined-admin-notification-email! <>, :google-auth? true)))
 
 (defn create-new-ldap-auth-user!
-  "Convenience for creating a new user via LDAP. This account is considered active immediately; thus all active admins will recieve an email right away."
+  "Convenience for creating a new user via LDAP. This account is considered active immediately; thus all active admins
+  will recieve an email right away."
   [first-name last-name email-address password]
   {:pre [(string? first-name) (string? last-name) (u/is-email? email-address)]}
   (db/insert! User :email      email-address
@@ -181,7 +183,7 @@
   (str (public-settings/site-url) "/auth/reset_password/" reset-token))
 
 
-;;; ------------------------------------------------------------ Permissions ------------------------------------------------------------
+;;; -------------------------------------------------- Permissions ---------------------------------------------------
 
 (defn permissions-set
   "Return a set of all permissions object paths that USER-OR-ID has been granted access to."
diff --git a/src/metabase/models/view_log.clj b/src/metabase/models/view_log.clj
index b69b78d86f05a8c0fbc17cea1224caf597b884ef..8430b56d4b4641c06cccf779fc78df99211d90dc 100644
--- a/src/metabase/models/view_log.clj
+++ b/src/metabase/models/view_log.clj
@@ -1,4 +1,5 @@
 (ns metabase.models.view-log
+  "The ViewLog is used to log an event where a given User views a given object such as a Table or Card (Question)."
   (:require [metabase.models.interface :as i]
             [metabase.util :as u]
             [toucan.models :as models]))
diff --git a/src/metabase/pulse.clj b/src/metabase/pulse.clj
index 33bf370c0a178209b6d8552bc0753237a9cdb9f0..c0d0d3ed0537a30c0d85ac28e3ba192b809d35d5 100644
--- a/src/metabase/pulse.clj
+++ b/src/metabase/pulse.clj
@@ -22,7 +22,7 @@
   (:import java.util.TimeZone
            metabase.models.card.CardInstance))
 
-;;; ## ---------------------------------------- PULSE SENDING ----------------------------------------
+;;; ------------------------------------------------- PULSE SENDING --------------------------------------------------
 
 
 ;; TODO: this is probably something that could live somewhere else and just be reused
@@ -221,7 +221,7 @@
         channel-ids (or channel-ids (mapv :id (:channels pulse)))]
     (when (should-send-notification? pulse results)
 
-      (when  (:alert_first_only pulse)
+      (when (:alert_first_only pulse)
         (db/delete! Pulse :id (:id pulse)))
 
       (for [channel-id channel-ids
diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index 46289343f9052b692a36f6475526231edece837d..7c5e9f575bc62ead6afe714cabc16c5c560aba96 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -44,6 +44,8 @@
 ;;; ## STYLES
 (def ^:private ^:const color-brand  "rgb(45,134,212)")
 (def ^:private ^:const color-purple "rgb(135,93,175)")
+(def ^:private ^:const color-gold   "#F9D45C")
+(def ^:private ^:const color-error  "#EF8C8C")
 (def ^:private ^:const color-gray-1 "rgb(248,248,248)")
 (def ^:private ^:const color-gray-2 "rgb(189,193,191)")
 (def ^:private ^:const color-gray-3 "rgb(124,131,129)")
@@ -87,7 +89,7 @@
 
 ;;; # ------------------------------------------------------------ HELPER FNS ------------------------------------------------------------
 
-(defn- style
+(defn style
   "Compile one or more CSS style maps into a string.
 
      (style {:font-weight 400, :color \"white\"}) -> \"font-weight: 400; color: white;\""
@@ -461,6 +463,7 @@
 
 (def ^:private external-link-url (io/resource "frontend_client/app/assets/img/external_link.png"))
 (def ^:private no-results-url    (io/resource "frontend_client/app/assets/img/pulse_no_results@2x.png"))
+(def ^:private attached-url      (io/resource "frontend_client/app/assets/img/attachment@2x.png"))
 
 (def ^:private external-link-image
   (delay
@@ -470,6 +473,10 @@
   (delay
    (make-image-bundle :attachment no-results-url)))
 
+(def ^:private attached-image
+  (delay
+   (make-image-bundle :attachment attached-url)))
+
 (defn- external-link-image-bundle [render-type]
   (case render-type
     :attachment @external-link-image
@@ -480,6 +487,11 @@
     :attachment @no-results-image
     :inline (make-image-bundle render-type no-results-url)))
 
+(defn- attached-image-bundle [render-type]
+  (case render-type
+    :attachment @attached-image
+    :inline (make-image-bundle render-type attached-url)))
+
 (defn- image-bundle->attachment [{:keys [render-type content-id image-url]}]
   (case render-type
     :attachment {content-id image-url}
@@ -550,10 +562,42 @@
      :content     [:div {:style (style {:text-align :center})}
                    [:img {:style (style {:width :104px})
                           :src   (:image-src image-bundle)}]
-                   [:div {:style (style {:margin-top :8px
+                   [:div {:style (style font-style
+                                        {:margin-top :8px
                                          :color      color-gray-4})}
                     "No results"]]}))
 
+(s/defn ^:private render:attached :- RenderedPulseCard
+  [render-type _ _]
+  (let [image-bundle (attached-image-bundle render-type)]
+    {:attachments (image-bundle->attachment image-bundle)
+     :content     [:div {:style (style {:text-align :center})}
+                   [:img {:style (style {:width :30px})
+                          :src   (:image-src image-bundle)}]
+                   [:div {:style (style font-style
+                                        {:margin-top :8px
+                                         :color      color-gray-4})}
+                    "This question has been included as a file attachment"]]}))
+
+(s/defn ^:private render:unknown :- RenderedPulseCard
+  [_ _]
+  {:attachments nil
+   :content     [:div {:style (style font-style
+                                     {:color       color-gold
+                                      :font-weight 700})}
+                 "We were unable to display this card."
+                 [:br]
+                 "Please view this card in Metabase."]})
+
+(s/defn ^:private render:error :- RenderedPulseCard
+  [_ _]
+  {:attachments nil
+   :content     [:div {:style (style font-style
+                                     {:color       color-error
+                                      :font-weight 700
+                                      :padding     :16px})}
+                 "An error occurred while displaying this card."]})
+
 (defn detect-pulse-card-type
   "Determine the pulse (visualization) type of a CARD, e.g. `:scalar` or `:bar`."
   [card data]
@@ -564,11 +608,11 @@
         col-2                     (col-2-rowfn (:cols data))
         aggregation               (-> card :dataset_query :query :aggregation first)]
     (cond
-      (or (= aggregation :rows)
-          (contains? #{:pin_map :state :country} (:display card))) nil
       (or (zero? row-count)
           ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters
           (= [[nil]] (-> data :rows)))                             :empty
+      (or (> col-count 3)
+          (contains? #{:pin_map :state :country} (:display card))) nil
       (and (= col-count 1)
            (= row-count 1))                                        :scalar
       (and (= col-count 2)
@@ -590,7 +634,7 @@
                                             :width         :100%})}
                      [:tbody
                       [:tr
-                       [:td [:span {:style header-style}
+                       [:td [:span {:style (style header-style)}
                              (-> card :name h)]]
                        [:td {:style (style {:text-align :right})}
                         (when *include-buttons*
@@ -598,6 +642,11 @@
                                  :width 16
                                  :src   (:image-src image-bundle)}])]]]]})))
 
+(defn- is-attached?
+  [card]
+  (or (:include_csv card)
+      (:include_xls card)))
+
 (s/defn ^:private render-pulse-card-body :- RenderedPulseCard
   [render-type timezone card {:keys [data error]}]
   (try
@@ -610,19 +659,12 @@
       :sparkline (render:sparkline render-type timezone card data)
       :bar       (render:bar       timezone card data)
       :table     (render:table     timezone card data)
-      {:attachments nil
-       :content     [:div {:style (style font-style
-                                         {:color       "#F9D45C"
-                                          :font-weight 700})}
-                     "We were unable to display this card." [:br] "Please view this card in Metabase."]})
+      (if (is-attached? card)
+        (render:attached render-type card data)
+        (render:unknown card data)))
     (catch Throwable e
       (log/error e (trs "Pulse card render error"))
-      {:attachments nil
-       :content     [:div {:style (style font-style
-                                         {:color       "#EF8C8C"
-                                          :font-weight 700
-                                          :padding     :16px})}
-                     "An error occurred while displaying this card."]})))
+      (render:error card data))))
 
 (s/defn ^:private render-pulse-card :- RenderedPulseCard
   "Render a single CARD for a `Pulse` to Hiccup HTML. RESULT is the QP results."
diff --git a/src/metabase/query_processor/annotate.clj b/src/metabase/query_processor/annotate.clj
index c5571a49f989d8465618f94f7086de6a991ce323..2ccf9cec989fd13960d5c00cfe0d7ca411c256cc 100644
--- a/src/metabase/query_processor/annotate.clj
+++ b/src/metabase/query_processor/annotate.clj
@@ -169,7 +169,8 @@
          ;; hardcoding these types is fine; In the future when we extend Expressions to handle more functionality
          ;; we'll want to introduce logic that associates a return type with a given expression. But this will work
          ;; for the purposes of a patch release.
-         (when (instance? ExpressionRef ag-field)
+         (when (or (instance? ExpressionRef ag-field)
+                   (instance? Expression ag-field))
            {:base-type    :type/Float
             :special-type :type/Number})))
 
diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj
index 2aa1be7ecb6380ef509fdd99562d0e919e9c1b94..83124eef448490984cb09b1feee51e4ba9a42c10 100644
--- a/src/metabase/query_processor/interface.clj
+++ b/src/metabase/query_processor/interface.clj
@@ -10,7 +10,7 @@
             [metabase.util.schema :as su]
             [schema.core :as s])
   (:import clojure.lang.Keyword
-           java.sql.Timestamp))
+           [java.sql Time Timestamp]))
 
 ;;; --------------------------------------------------- CONSTANTS ----------------------------------------------------
 
@@ -182,6 +182,15 @@
   clojure.lang.Named
   (getName [_] (name field)))
 
+;; TimeField is just a field wrapper that indicates string should be interpretted as a time
+(s/defrecord TimeField [field :- (s/cond-pre Field FieldLiteral)]
+  clojure.lang.Named
+  (getName [_] (name field)))
+
+(s/defrecord TimeValue [value       :- Time
+                        field       :- TimeField
+                        timezone-id :- (s/maybe String)])
+
 (def binning-strategies
   "Valid binning strategies for a `BinnedField`"
   #{:num-bins :bin-width :default})
diff --git a/src/metabase/query_processor/middleware/format_rows.clj b/src/metabase/query_processor/middleware/format_rows.clj
index 3edf89bad7095fff8811759b61088ba0749a24e9..114a60ed307b0bc769b6eb2864753734ab42e73d 100644
--- a/src/metabase/query_processor/middleware/format_rows.clj
+++ b/src/metabase/query_processor/middleware/format_rows.clj
@@ -7,12 +7,17 @@
   (let [timezone (or report-timezone (System/getProperty "user.timezone"))]
     (for [row rows]
       (for [v row]
-        (if (u/is-temporal? v)
-          ;; NOTE: if we don't have an explicit report-timezone then use the JVM timezone
-          ;;       this ensures alignment between the way dates are processed by JDBC and our returned data
-          ;;       GH issues: #2282, #2035
+        ;; NOTE: if we don't have an explicit report-timezone then use the JVM timezone
+        ;;       this ensures alignment between the way dates are processed by JDBC and our returned data
+        ;;       GH issues: #2282, #2035
+        (cond
+          (u/is-time? v)
+          (u/format-time v timezone)
+
+          (u/is-temporal? v)
           (u/->iso-8601-datetime v timezone)
-          v)))))
+
+          :else v)))))
 
 (defn format-rows
   "Format individual query result values as needed.  Ex: format temporal values as iso8601 strings w/ timezone."
diff --git a/src/metabase/query_processor/middleware/limit.clj b/src/metabase/query_processor/middleware/limit.clj
index 0568f7c7680764de7de80b023518b792c785b7f0..d0637ded3d9ee6e7ff485abd606dad3795dcaa66 100644
--- a/src/metabase/query_processor/middleware/limit.clj
+++ b/src/metabase/query_processor/middleware/limit.clj
@@ -4,7 +4,8 @@
                                       [util :as qputil])))
 
 (defn limit
-  "Add an implicit `limit` clause to MBQL queries without any aggregations, and limit the maximum number of rows that can be returned in post-processing."
+  "Add an implicit `limit` clause to MBQL queries without any aggregations, and limit the maximum number of rows that
+  can be returned in post-processing."
   [qp]
   (fn [{{:keys [max-results max-results-bare-rows]} :constraints, :as query}]
     (let [query   (cond-> query
diff --git a/src/metabase/query_processor/middleware/parameters/dates.clj b/src/metabase/query_processor/middleware/parameters/dates.clj
index 6b5bddd2490ffa1a002ba68e194f5358cad1ecd3..1cf075c89a5d4cfcbf3d27837cbf2b0739b62305 100644
--- a/src/metabase/query_processor/middleware/parameters/dates.clj
+++ b/src/metabase/query_processor/middleware/parameters/dates.clj
@@ -13,7 +13,6 @@
 ;; See https://github.com/metabase/metabase/pull/4607#issuecomment-290884313 how we could support
 ;; hour/minute granularity in field parameter queries.
 
-
 (defn- day-range
   [^DateTime start, ^DateTime end]
   {:end   end
@@ -63,9 +62,9 @@
   (tf/parse (tf/formatters :date-opt-time) date))
 
 
-;;; +-------------------------------------------------------------------------------------------------------+
-;;; |                                    DATE STRING DECODERS                                               |
-;;; +-------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                              DATE STRING DECODERS                                              |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 ;; For parsing date strings and producing either a date range (for raw SQL parameter substitution) or a MBQL clause
 
@@ -80,9 +79,10 @@
 
 
 (defn- regex->parser
-  "Takes a regex and labels matching the regex capturing groups. Returns a parser which
-  takes a parameter value, validates the value against regex and gives a map of labels
-  and group values. Respects the following special label names:
+  "Takes a regex and labels matching the regex capturing groups. Returns a parser which takes a parameter value,
+  validates the value against regex and gives a map of labels and group values. Respects the following special label
+  names:
+
       :unit – finds a matching date unit and merges date unit operations to the result
       :int-value – converts the group value to integer
       :date, :date1, date2 – converts the group value to absolute date"
@@ -190,8 +190,8 @@
   (concat relative-date-string-decoders absolute-date-string-decoders))
 
 (defn- execute-decoders
-  "Returns the first successfully decoded value, run through both
-   parser and a range/filter decoder depending on `decoder-type`."
+  "Returns the first successfully decoded value, run through both parser and a range/filter decoder depending on
+  `decoder-type`."
   [decoders decoder-type decoder-param date-string]
   (some (fn [{parser :parser, parser-result-decoder decoder-type}]
           (when-let [parser-result (parser date-string)]
@@ -199,19 +199,19 @@
         decoders))
 
 (defn date-string->range
-  "Takes a string description of a date range such as 'lastmonth' or '2016-07-15~2016-08-6' and
-   return a MAP with `:start` and `:end` as iso8601 string formatted dates, respecting the given timezone."
+  "Takes a string description of a date range such as 'lastmonth' or '2016-07-15~2016-08-6' and return a MAP with
+  `:start` and `:end` as iso8601 string formatted dates, respecting the given timezone."
   [date-string report-timezone]
   (let [tz                 (t/time-zone-for-id report-timezone)
         formatter-local-tz (tf/formatter "yyyy-MM-dd" tz)
         formatter-no-tz    (tf/formatter "yyyy-MM-dd")
         today              (.withTimeAtStartOfDay (t/to-time-zone (t/now) tz))]
-    ;; Relative dates respect the given time zone because a notion like "last 7 days" might mean a different range of days
-    ;; depending on the user timezone
+    ;; Relative dates respect the given time zone because a notion like "last 7 days" might mean a different range of
+    ;; days depending on the user timezone
     (or (->> (execute-decoders relative-date-string-decoders :range today date-string)
              (m/map-vals (partial tf/unparse formatter-local-tz)))
-        ;; Absolute date ranges don't need the time zone conversion because in SQL the date ranges are compared against
-        ;; the db field value that is casted granularity level of a day in the db time zone
+        ;; Absolute date ranges don't need the time zone conversion because in SQL the date ranges are compared
+        ;; against the db field value that is casted granularity level of a day in the db time zone
         (->> (execute-decoders absolute-date-string-decoders :range nil date-string)
              (m/map-vals (partial tf/unparse formatter-no-tz))))))
 
diff --git a/src/metabase/query_processor/middleware/parameters/mbql.clj b/src/metabase/query_processor/middleware/parameters/mbql.clj
index 48f8b454c573d31978c528f7e3d4d867f93991cc..3b9f1bdcbc162146ce2274f950eed9473d70d6b4 100644
--- a/src/metabase/query_processor/middleware/parameters/mbql.clj
+++ b/src/metabase/query_processor/middleware/parameters/mbql.clj
@@ -45,9 +45,16 @@
   values in the queries themselves)."
   [query-dict [{:keys [target value], :as param} & rest]]
   (cond
-    (not param)      query-dict
+    (not param)
+    query-dict
+
     (or (not target)
-        (not value)) (recur query-dict rest)
-    :else            (let [filter-subclause (build-filter-clause param)
-                           query            (assoc-in query-dict [:query :filter] (merge-filter-clauses (get-in query-dict [:query :filter]) filter-subclause))]
-                       (recur query rest))))
+        (not value))
+    (recur query-dict rest)
+
+    :else
+    (let [filter-subclause (build-filter-clause param)
+          query            (assoc-in query-dict [:query :filter] (merge-filter-clauses
+                                                                  (get-in query-dict [:query :filter])
+                                                                  filter-subclause))]
+      (recur query rest))))
diff --git a/src/metabase/query_processor/middleware/resolve.clj b/src/metabase/query_processor/middleware/resolve.clj
index fd797772a60938fbb5e73029ad3d9f184b00b2a9..f627d5afdecde7d001892e84748d6e4755364eb4 100644
--- a/src/metabase/query_processor/middleware/resolve.clj
+++ b/src/metabase/query_processor/middleware/resolve.clj
@@ -26,7 +26,7 @@
              [hydrate :refer [hydrate]]])
   (:import java.util.TimeZone
            [metabase.query_processor.interface DateTimeField DateTimeValue ExpressionRef Field FieldPlaceholder
-            RelativeDatetime RelativeDateTimeValue Value ValuePlaceholder]))
+            RelativeDatetime RelativeDateTimeValue TimeField TimeValue Value ValuePlaceholder]))
 
 ;;; ---------------------------------------------------- UTIL FNS ----------------------------------------------------
 
@@ -196,6 +196,9 @@
       (i/map->DateTimeField {:field field
                              :unit  (or datetime-unit :day)}) ; default to `:day` if a unit wasn't specified
 
+      (isa? base-type :type/Time)
+      (i/map->TimeField {:field field})
+
       binning-strategy
       (resolve-binned-field this field)
 
@@ -245,7 +248,24 @@
         nil
 
         :else
-        (throw (Exception. (format "Invalid value '%s': expected a DateTime." value)))))))
+        (throw (Exception. (format "Invalid value '%s': expected a DateTime." value))))))
+
+  TimeField
+  (parse-value [this value]
+    (let [tz-id              ^String (setting/get :report-timezone)
+          tz                 (when tz-id
+                               (TimeZone/getTimeZone tz-id))
+          parsed-string-time (some-> value
+                                     (u/str->time tz))]
+      (cond
+        parsed-string-time
+        (s/validate TimeValue (i/map->TimeValue {:field this, :value parsed-string-time :timezone-id tz-id}))
+
+        (nil? value)
+        nil
+
+        :else
+        (throw (Exception. (format "Invalid value '%s': expected a Time." value)))))))
 
 (defn- value-ph-resolve-field
   "Attempt to resolve the `Field` for a `ValuePlaceholder`. Return a resolved `Value` or `DateTimeValue`."
diff --git a/src/metabase/sync/analyze/classifiers/text_fingerprint.clj b/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
index 1208f590e823b702f11f59821f8b224ed23be0c6..3caaa64926fe35f20c7a6655b7ae4919520f2e0d 100644
--- a/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
+++ b/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
@@ -22,14 +22,15 @@
 
 
 (def ^:private percent-key->special-type
-  "Map of keys inside the `TextFingerprint` to the corresponding special types we should mark a Field as if the value of the key
-   is over `percent-valid-thresold`."
+  "Map of keys inside the `TextFingerprint` to the corresponding special types we should mark a Field as if the value of
+  the key is over `percent-valid-thresold`."
   {:percent-json  :type/SerializedJSON
    :percent-url   :type/URL
    :percent-email :type/Email})
 
 (s/defn ^:private infer-special-type-for-text-fingerprint :- (s/maybe su/FieldType)
-  "Check various percentages inside the TEXT-FINGERPRINT and return the corresponding special type to mark the Field as if the percent passes the threshold."
+  "Check various percentages inside the TEXT-FINGERPRINT and return the corresponding special type to mark the Field
+  as if the percent passes the threshold."
   [text-fingerprint :- i/TextFingerprint]
   (some (fn [[percent-key special-type]]
           (when (percent-key-below-threshold? text-fingerprint percent-key)
@@ -45,6 +46,7 @@
     (when-not (:special_type field)
       (when-let [text-fingerprint (get-in fingerprint [:type :type/Text])]
         (when-let [inferred-special-type (infer-special-type-for-text-fingerprint text-fingerprint)]
-          (log/debug (format "Based on the fingerprint of %s, we're marking it as %s." (sync-util/name-for-logging field) inferred-special-type))
+          (log/debug (format "Based on the fingerprint of %s, we're marking it as %s."
+                             (sync-util/name-for-logging field) inferred-special-type))
           (assoc field
             :special_type inferred-special-type))))))
diff --git a/src/metabase/sync/field_values.clj b/src/metabase/sync/field_values.clj
index 9bc8dffc7a3001acb56f7c2ca4346dd73c35cdc1..ce85aab842a09dc874a098d44da075e852d1b2f9 100644
--- a/src/metabase/sync/field_values.clj
+++ b/src/metabase/sync/field_values.clj
@@ -37,6 +37,7 @@
   "Update the cached FieldValues (distinct values for categories and certain other fields that are shown
    in widgets like filters) for the Tables in DATABASE (as needed)."
   [database :- i/DatabaseInstance]
-  (sync-util/sync-operation :cache-field-values database (format "Cache field values in %s" (sync-util/name-for-logging database))
+  (sync-util/sync-operation :cache-field-values database (format "Cache field values in %s"
+                                                                 (sync-util/name-for-logging database))
     (doseq [table (sync-util/db->sync-tables database)]
       (update-field-values-for-table! table))))
diff --git a/src/metabase/util.clj b/src/metabase/util.clj
index ae3fb286f8b868d34f2b50455219f58d5828d192..39a19ab146dfb6ac5d74d1fed4a34871d04739ec 100644
--- a/src/metabase/util.clj
+++ b/src/metabase/util.clj
@@ -20,7 +20,7 @@
             [ring.util.codec :as codec])
   (:import clojure.lang.Keyword
            [java.net InetAddress InetSocketAddress Socket]
-           [java.sql SQLException Timestamp]
+           [java.sql SQLException Time Timestamp]
            [java.text Normalizer Normalizer$Form]
            [java.util Calendar Date TimeZone]
            org.joda.time.DateTime
@@ -101,7 +101,7 @@
   (->iso-8601-datetime ^String [this timezone-id]
     "Coerce object to an ISO8601 date-time string such as \"2015-11-18T23:55:03.841Z\" with a given TIMEZONE."))
 
-(def ^:private ISO8601Formatter
+(def ^:private ^{:arglists '([timezone-id])} ISO8601Formatter
   ;; memoize this because the formatters are static. They must be distinct per timezone though.
   (memoize (fn [timezone-id]
              (if timezone-id
@@ -115,6 +115,25 @@
   java.sql.Timestamp     (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) (coerce/from-sql-time this)))
   org.joda.time.DateTime (->iso-8601-datetime [this timezone-id] (time/unparse (ISO8601Formatter timezone-id) this)))
 
+(def ^:private ^{:arglists '([timezone-id])} time-formatter
+  ;; memoize this because the formatters are static. They must be distinct per timezone though.
+  (memoize (fn [timezone-id]
+             (if timezone-id
+               (time/with-zone (time/formatters :time) (t/time-zone-for-id timezone-id))
+               (time/formatters :time)))))
+
+(defn format-time
+  "Returns a string representation of the time found in `T`"
+  [t time-zone-id]
+  (time/unparse (time-formatter time-zone-id) (coerce/to-date-time t)))
+
+(defn is-time?
+  "Returns true if `V` is a Time object"
+  [v]
+  (and v (instance? Time v)))
+
+;;; ## Date Stuff
+
 (defn is-temporal?
   "Is VALUE an instance of a datetime class like `java.util.Date` or `org.joda.time.DateTime`?"
   [v]
@@ -858,6 +877,20 @@
     (apply update m k f args)
     m))
 
+(defn- str->date-time-with-formatters
+  "Attempt to parse `DATE-STR` using `FORMATTERS`. First successful
+  parse is returned, or nil"
+  ([formatters date-str]
+   (str->date-time-with-formatters formatters date-str nil))
+  ([formatters ^String date-str ^TimeZone tz]
+   (let [dtz (some-> tz .getID t/time-zone-for-id)]
+     (first
+      (for [formatter formatters
+            :let [formatter-with-tz (time/with-zone formatter dtz)
+                  parsed-date (ignore-exceptions (time/parse formatter-with-tz date-str))]
+            :when parsed-date]
+        parsed-date)))))
+
 (def ^:private date-time-with-millis-no-t
   "This primary use for this formatter is for Dates formatted by the built-in SQLite functions"
   (->DateTimeFormatter "yyyy-MM-dd HH:mm:ss.SSS"))
@@ -878,11 +911,20 @@
   was unable to be parsed."
   (^org.joda.time.DateTime [^String date-str]
    (str->date-time date-str nil))
-  (^org.joda.time.DateTime [^String date-str, ^TimeZone tz]
-   (let [dtz (some-> tz .getID t/time-zone-for-id)]
-     (first
-      (for [formatter ordered-date-parsers
-            :let [formatter-with-tz (time/with-zone formatter dtz)
-                  parsed-date (ignore-exceptions (time/parse formatter-with-tz date-str))]
-            :when parsed-date]
-        parsed-date)))))
+  ([^String date-str ^TimeZone tz]
+   (str->date-time-with-formatters ordered-date-parsers date-str tz)))
+
+(def ^:private ordered-time-parsers
+  (let [most-likely-default-formatters [:hour-minute :hour-minute-second :hour-minute-second-fraction]]
+    (concat (map time/formatters most-likely-default-formatters)
+            [(time/formatter "HH:mmZ") (time/formatter "HH:mm:SSZ") (time/formatter "HH:mm:SS.SSSZ")])))
+
+(defn str->time
+  "Parse `TIME-STR` and return a `java.sql.Time` instance. Returns nil
+  if `TIME-STR` can't be parsed."
+  ([^String date-str]
+   (str->date-time date-str nil))
+  ([^String date-str ^TimeZone tz]
+   (some-> (str->date-time-with-formatters ordered-time-parsers date-str tz)
+           coerce/to-long
+           Time.)))
diff --git a/src/metabase/util/export.clj b/src/metabase/util/export.clj
index 2b243ef652e6214c520a3dbdc27bb78a9ad36577..840dd3f75d41d7f054d9fa4b272c71cb784e08f8 100644
--- a/src/metabase/util/export.clj
+++ b/src/metabase/util/export.clj
@@ -2,11 +2,9 @@
   (:require [cheshire.core :as json]
             [clojure.data.csv :as csv]
             [dk.ative.docjure.spreadsheet :as spreadsheet])
-  (:import [java.io ByteArrayInputStream ByteArrayOutputStream]
+  (:import [java.io ByteArrayInputStream ByteArrayOutputStream File]
            org.apache.poi.ss.usermodel.Cell))
 
-
-
 ;; add a generic implementation for the method that writes values to XLSX cells that just piggybacks off the
 ;; implementations we've already defined for encoding things as JSON. These implementations live in
 ;; `metabase.middleware`.
@@ -20,6 +18,12 @@
                                (json/parse-string keyword)
                                :v))))
 
+(defn- results->cells
+  "Convert the resultset to a seq of rows with the first row as a header"
+  [results]
+  (cons (map :display_name (get-in results [:result :data :cols]))
+        (get-in results [:result :data :rows])))
+
 (defn- export-to-xlsx [columns rows]
   (let [wb  (spreadsheet/create-workbook "Query result" (cons (mapv name columns) rows))
         ;; note: byte array streams don't need to be closed
@@ -27,11 +31,25 @@
     (spreadsheet/save-workbook! out wb)
     (ByteArrayInputStream. (.toByteArray out))))
 
+(defn export-to-xlsx-file
+  "Write an XLS file to `FILE` with the header a and rows found in `RESULTS`"
+  [^File file results]
+  (let [file-path (.getAbsolutePath file)]
+    (->> (results->cells results)
+         (spreadsheet/create-workbook "Query result" )
+         (spreadsheet/save-workbook! file-path))))
+
 (defn- export-to-csv [columns rows]
   (with-out-str
     ;; turn keywords into strings, otherwise we get colons in our output
     (csv/write-csv *out* (into [(mapv name columns)] rows))))
 
+(defn export-to-csv-writer
+  "Write a CSV to `FILE` with the header a and rows found in `RESULTS`"
+  [^File file results]
+  (with-open [fw (java.io.FileWriter. file)]
+    (csv/write-csv fw (results->cells results))))
+
 (defn- export-to-json [columns rows]
   (for [row rows]
     (zipmap columns row)))
diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj
index c9b179d530ab3cc25be40fe69137621d3886690a..bc636d97ca0145deab44bccd8ff7179602552e55 100644
--- a/src/metabase/util/honeysql_extensions.clj
+++ b/src/metabase/util/honeysql_extensions.clj
@@ -134,6 +134,7 @@
 (defn ->timestamp                "CAST X to a `timestamp`."                [x] (cast :timestamp x))
 (defn ->timestamp-with-time-zone "CAST X to a `timestamp with time zone`." [x] (cast "timestamp with time zone" x))
 (defn ->integer                  "CAST X to a `integer`."                  [x] (cast :integer x))
+(defn ->time                     "CAST X to a `time` datatype"             [x] (cast :time x))
 
 ;;; Random SQL fns. Not all DBs support all these!
 (def ^{:arglists '([& exprs])} floor   "SQL `floor` function."  (partial hsql/call :floor))
diff --git a/src/metabase/query.clj b/src/metabase/util/query.clj
similarity index 60%
rename from src/metabase/query.clj
rename to src/metabase/util/query.clj
index 680b8f9138adab13d1c5feb0dc470aaaae93c58c..3fd55bb12a7aa78d1b3c43f43ad06bc28eb71ab8 100644
--- a/src/metabase/query.clj
+++ b/src/metabase/util/query.clj
@@ -1,21 +1,22 @@
-(ns metabase.query
-  "Functions for dealing with structured queries."
+(ns metabase.util.query
+  "Utility functions for dealing with structured queries."
   (:require [clojure.core.match :refer [match]]))
 
-;; TODO These functions are written for MBQL '95. MBQL '98 doesn't require that clause identifiers be uppercased, or strings.
-;; Also, I'm not convinced `:segment` or `:metric` are actually part of MBQL since they aren't in the MBQL '98 reference
+;; TODO These functions are written for MBQL '95. MBQL '98 doesn't require that clause identifiers be uppercased, or
+;; strings. Also, I'm not convinced `:segment` or `:metric` are actually part of MBQL since they aren't in the MBQL
+;; '98 reference
 
 (defn- parse-filter-subclause [subclause]
   (match subclause
-         ["SEGMENT" (segment-id :guard integer?)] segment-id
-         _                                        nil))
+    ["SEGMENT" (segment-id :guard integer?)] segment-id
+    _                                        nil))
 
 ;; TODO This doesn't handle the NOT clause
 (defn- parse-filter [clause]
   (match clause
-         ["AND" & subclauses] (mapv parse-filter subclauses)
-         ["OR" & subclauses]  (mapv parse-filter subclauses)
-         subclause            (parse-filter-subclause subclause)))
+    ["AND" & subclauses] (mapv parse-filter subclauses)
+    ["OR" & subclauses]  (mapv parse-filter subclauses)
+    subclause            (parse-filter-subclause subclause)))
 
 (defn extract-segment-ids
   "Return the IDs of all `Segments` in the query. (I think that's what this does? :flushed:)"
diff --git a/test/metabase/api/alert_test.clj b/test/metabase/api/alert_test.clj
index 8522fdeeb1b245dcbf41fc61db48e6d718df7315..a7294ee676e2f23389afe2485bf9ba22c92777e3 100644
--- a/test/metabase/api/alert_test.clj
+++ b/test/metabase/api/alert_test.clj
@@ -29,7 +29,7 @@
   (-> card
       (select-keys [:name :description :display])
       (update :display name)
-      (assoc :id true)))
+      (assoc :id true, :include_csv false, :include_xls false)))
 
 (defn- recipient-details [user-kwd]
   (-> user-kwd
@@ -156,6 +156,7 @@
 ;; Check creation of a new rows alert with email notification
 (tt/expect-with-temp [Card [card1 {:name "My question"}]]
   [(-> (default-alert card1)
+       (assoc-in [:card :include_csv] true)
        (update-in [:channels 0] merge {:schedule_hour 12, :schedule_type "daily", :recipients []}))
    (rasta-new-alert-email {"has any results" true})]
   (with-alert-setup
@@ -182,6 +183,7 @@
 (tt/expect-with-temp [Card [card1 {:name "My question"}]]
   [(-> (default-alert card1)
        (assoc :creator (user-details :crowberto))
+       (assoc-in [:card :include_csv] true)
        (update-in [:channels 0] merge {:schedule_hour 12, :schedule_type "daily", :recipients (set (map recipient-details [:rasta :crowberto]))}))
    (merge (et/email-to :crowberto {:subject "You setup an alert",
                                    :body {"https://metabase.com/testmb" true,
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index 9ca79bf2cb17643c73a3f3d4e5983f9724675f23..c79305af0615355491ef2e3e4fc2fe52e610e545 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -11,6 +11,7 @@
              [dashboard :as dashboard-api]]
             [metabase.models
              [card :refer [Card]]
+             [collection :refer [Collection]]
              [dashboard :refer [Dashboard]]
              [dashboard-card :refer [DashboardCard retrieve-dashboard-card]]
              [dashboard-card-series :refer [DashboardCardSeries]]
@@ -143,6 +144,19 @@
                   DashboardCard [_                  {:dashboard_id dashboard-id, :card_id card-id}]]
     (dashboard-response ((user->client :rasta) :get 200 (format "dashboard/%d" dashboard-id)))))
 
+;; ## GET /api/dashboard/:id with a series, should fail if the user doesn't have access to the collection
+(expect
+  "You don't have permissions to do that."
+  (tt/with-temp* [Collection          [{coll-id :id}      {:name "Collection 1"}]
+                  Dashboard           [{dashboard-id :id} {:name       "Test Dashboard"
+                                                           :creator_id (user->id :crowberto)}]
+                  Card                [{card-id :id}      {:name          "Dashboard Test Card"
+                                                           :collection_id coll-id}]
+                  Card                [{card-id2 :id}     {:name          "Dashboard Test Card 2"
+                                                           :collection_id coll-id}]
+                  DashboardCard       [{dbc_id :id}       {:dashboard_id dashboard-id, :card_id card-id}]
+                  DashboardCardSeries [_                  {:dashboardcard_id dbc_id, :card_id card-id2, :position 0}]]
+    ((user->client :rasta) :get 403 (format "dashboard/%d" dashboard-id))))
 
 ;; ## PUT /api/dashboard/:id
 (expect
diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj
index 83c41777d46a5ce19a20752ce7a00e033f47d9d5..7b1331b64311ac418a046240a81cab1bf0fbae08 100644
--- a/test/metabase/api/database_test.clj
+++ b/test/metabase/api/database_test.clj
@@ -481,10 +481,13 @@
                                    :result_metadata [{:name "age_in_bird_years"}])]]
   (saved-questions-virtual-db
     (assoc (virtual-table-for-card card)
-      :fields [{:name         "age_in_bird_years"
-                :table_id     (str "card__" (u/get-id card))
-                :id           ["field-literal" "age_in_bird_years" "type/*"]
-                :special_type nil}]))
+      :fields [{:name                     "age_in_bird_years"
+                :table_id                 (str "card__" (u/get-id card))
+                :id                       ["field-literal" "age_in_bird_years" "type/*"]
+                :special_type             nil
+                :base_type                nil
+                :default_dimension_option nil
+                :dimension_options        []}]))
   ((user->client :crowberto) :get 200 (format "database/%d/metadata" database/virtual-id)))
 
 ;; if no eligible Saved Questions exist the virtual DB metadata endpoint should just return `nil`
diff --git a/test/metabase/api/pulse_test.clj b/test/metabase/api/pulse_test.clj
index f410e46ef839a2d24210ddd5132ad1200ab9c814..cf6682a884376088052f52217fc634e61bfaa684 100644
--- a/test/metabase/api/pulse_test.clj
+++ b/test/metabase/api/pulse_test.clj
@@ -31,7 +31,8 @@
 
 (defn- pulse-card-details [card]
   (-> (select-keys card [:id :name :description :display])
-      (update :display name)))
+      (update :display name)
+      (assoc :include_csv false :include_xls false)))
 
 (defn- pulse-channel-details [channel]
   (select-keys channel [:schedule_type :schedule_details :channel_type :updated_at :details :pulse_id :id :enabled
@@ -124,6 +125,38 @@
                                                                   :skip_if_empty false}))
         (update :channels remove-extra-channels-fields))))
 
+;; Create a pulse with a csv and xls
+(tt/expect-with-temp [Card [card1]
+                      Card [card2]]
+  {:name          "A Pulse"
+   :creator_id    (user->id :rasta)
+   :creator       (user-details (fetch-user :rasta))
+   :created_at    true
+   :updated_at    true
+   :cards         [(assoc (pulse-card-details card1) :include_csv true :include_xls true)
+                   (pulse-card-details card2)]
+   :channels      [(merge pulse-channel-defaults
+                          {:channel_type  "email"
+                           :schedule_type "daily"
+                           :schedule_hour 12
+                           :recipients    []})]
+   :skip_if_empty false}
+  (-> (pulse-response ((user->client :rasta) :post 200 "pulse" {:name          "A Pulse"
+                                                                :cards         [{:id          (:id card1)
+                                                                                 :include_csv true
+                                                                                 :include_xls true}
+                                                                                {:id          (:id card2)
+                                                                                 :include_csv false
+                                                                                 :include_xls false}]
+                                                                :channels      [{:enabled       true
+                                                                                 :channel_type  "email"
+                                                                                 :schedule_type "daily"
+                                                                                 :schedule_hour 12
+                                                                                 :schedule_day  nil
+                                                                                 :recipients    []}]
+                                                                :skip_if_empty false}))
+      (update :channels remove-extra-channels-fields)))
+
 
 ;; ## PUT /api/pulse
 
diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj
index 070149467584e9172b9c1afc3a02a363cfdf864a..881e39d6c9ccabf242b65b136d7654bd8c00f49e 100644
--- a/test/metabase/api/table_test.clj
+++ b/test/metabase/api/table_test.clj
@@ -19,6 +19,7 @@
              [permissions :as perms]
              [permissions-group :as perms-group]
              [table :as table :refer [Table]]]
+            [metabase.query-processor-test :as qpt]
             [metabase.test
              [data :as data]
              [util :as tu :refer [match-$]]]
@@ -158,11 +159,14 @@
     (perms/delete-related-permissions! (perms-group/all-users) (perms/object-path database-id))
     ((user->client :rasta) :get 403 (str "table/" table-id))))
 
-(defn- query-metadata-defaults []
+(defn- default-dimension-options []
   (->> #'table-api/dimension-options-for-response
        var-get
-       walk/keywordize-keys
-       (assoc (table-defaults) :dimension_options)))
+       walk/keywordize-keys))
+
+(defn- query-metadata-defaults []
+  (-> (table-defaults)
+      (assoc :dimension_options (default-dimension-options))))
 
 ;; ## GET /api/table/:id/query_metadata
 (expect
@@ -435,21 +439,59 @@
                                                   :type     :native
                                                   :native   {:query (format "SELECT NAME, ID, PRICE, LATITUDE FROM VENUES")}}}]]
   (let [card-virtual-table-id (str "card__" (u/get-id card))]
-    {:display_name "Go Dubs!"
-     :schema       "Everything else"
-     :db_id        database/virtual-id
-     :id           card-virtual-table-id
-     :description  nil
-     :fields       (for [[field-name display-name base-type] [["NAME"     "Name"     "type/Text"]
-                                                              ["ID"       "ID"       "type/Integer"]
-                                                              ["PRICE"    "Price"    "type/Integer"]
-                                                              ["LATITUDE" "Latitude" "type/Float"]]]
-                     {:name         field-name
-                      :display_name display-name
-                      :base_type    base-type
-                      :table_id     card-virtual-table-id
-                      :id           ["field-literal" field-name base-type]
-                      :special_type nil})})
+    {:display_name      "Go Dubs!"
+     :schema            "Everything else"
+     :db_id             database/virtual-id
+     :id                card-virtual-table-id
+     :description       nil
+     :dimension_options (default-dimension-options)
+     :fields            (for [[field-name display-name base-type] [["NAME"     "Name"     "type/Text"]
+                                                                   ["ID"       "ID"       "type/Integer"]
+                                                                   ["PRICE"    "Price"    "type/Integer"]
+                                                                   ["LATITUDE" "Latitude" "type/Float"]]]
+                          {:name                     field-name
+                           :display_name             display-name
+                           :base_type                base-type
+                           :table_id                 card-virtual-table-id
+                           :id                       ["field-literal" field-name base-type]
+                           :special_type             nil
+                           :default_dimension_option nil
+                           :dimension_options        []})})
+  (do
+    ;; run the Card which will populate its result_metadata column
+    ((user->client :crowberto) :post 200 (format "card/%d/query" (u/get-id card)))
+    ;; Now fetch the metadata for this "table"
+    ((user->client :crowberto) :get 200 (format "table/card__%d/query_metadata" (u/get-id card)))))
+
+;; Test date dimensions being included with a nested query
+(tt/expect-with-temp [Card [card {:name          "Users"
+                                  :database_id   (data/id)
+                                  :dataset_query {:database (data/id)
+                                                  :type     :native
+                                                  :native   {:query (format "SELECT NAME, LAST_LOGIN FROM USERS")}}}]]
+  (let [card-virtual-table-id (str "card__" (u/get-id card))]
+    {:display_name      "Users"
+     :schema            "Everything else"
+     :db_id             database/virtual-id
+     :id                card-virtual-table-id
+     :description       nil
+     :dimension_options (default-dimension-options)
+     :fields            [{:name                     "NAME"
+                          :display_name             "Name"
+                          :base_type                "type/Text"
+                          :table_id                 card-virtual-table-id
+                          :id                       ["field-literal" "NAME" "type/Text"]
+                          :special_type             nil
+                          :default_dimension_option nil
+                          :dimension_options        []}
+                         {:name                     "LAST_LOGIN"
+                          :display_name             "Last Login"
+                          :base_type                "type/DateTime"
+                          :table_id                 card-virtual-table-id
+                          :id                       ["field-literal" "LAST_LOGIN" "type/DateTime"]
+                          :special_type             nil
+                          :default_dimension_option (var-get #'table-api/date-default-index)
+                          :dimension_options        (var-get #'table-api/datetime-dimension-indexes)}]})
   (do
     ;; run the Card which will populate its result_metadata column
     ((user->client :crowberto) :post 200 (format "card/%d/query" (u/get-id card)))
@@ -615,3 +657,9 @@
  (var-get #'table-api/datetime-dimension-indexes)
  (let [response ((user->client :rasta) :get 200 (format "table/%d/query_metadata" (data/id :checkins)))]
    (dimension-options-for-field response "date")))
+
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift}
+  []
+  (data/with-db (data/get-or-create-database! defs/test-data-with-time)
+    (let [response ((user->client :rasta) :get 200 (format "table/%d/query_metadata" (data/id :users)))]
+      (dimension-options-for-field response "last_login_time"))))
diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj
index 2365f5950683febe2b8607cf7cdb2de1e25513c6..0e9605bf517dfc433c8c65948e6f41e20573f961 100644
--- a/test/metabase/driver/mongo_test.clj
+++ b/test/metabase/driver/mongo_test.clj
@@ -177,11 +177,11 @@
 
 ;;; Check that we support Mongo BSON ID and can filter by it (#1367)
 (i/def-database-definition ^:private with-bson-ids
-  ["birds"
-   [{:field-name "name", :base-type :type/Text}
-    {:field-name "bird_id", :base-type :type/MongoBSONID}]
-   [["Rasta Toucan" (ObjectId. "012345678901234567890123")]
-    ["Lucky Pigeon" (ObjectId. "abcdefabcdefabcdefabcdef")]]])
+  [["birds"
+     [{:field-name "name", :base-type :type/Text}
+      {:field-name "bird_id", :base-type :type/MongoBSONID}]
+     [["Rasta Toucan" (ObjectId. "012345678901234567890123")]
+      ["Lucky Pigeon" (ObjectId. "abcdefabcdefabcdefabcdef")]]]])
 
 (datasets/expect-with-engine :mongo
   [[2 "Lucky Pigeon" (ObjectId. "abcdefabcdefabcdefabcdef")]]
diff --git a/test/metabase/driver/mysql_test.clj b/test/metabase/driver/mysql_test.clj
index b13b5dff4d504547ab175b9704425907a52faf4c..bda9d3045e1c4eb47dc723dd03cf8382306acf83 100644
--- a/test/metabase/driver/mysql_test.clj
+++ b/test/metabase/driver/mysql_test.clj
@@ -24,9 +24,9 @@
 ;; MySQL allows 0000-00-00 dates, but JDBC does not; make sure that MySQL is converting them to NULL when returning
 ;; them like we asked
 (def-database-definition ^:private ^:const all-zero-dates
-  ["exciting-moments-in-history"
-   [{:field-name "moment", :base-type :type/DateTime}]
-   [["0000-00-00"]]])
+  [["exciting-moments-in-history"
+     [{:field-name "moment", :base-type :type/DateTime}]
+     [["0000-00-00"]]]])
 
 (expect-with-engine :mysql
   [[1 nil]]
@@ -52,12 +52,12 @@
 ;; correct additional options, we should be able to change that -- see
 ;; https://github.com/metabase/metabase/issues/3506
 (def-database-definition ^:private ^:const tiny-int-ones
-  ["number-of-cans"
-   [{:field-name "thing",          :base-type :type/Text}
-    {:field-name "number-of-cans", :base-type {:native "tinyint(1)"}}]
-   [["Six Pack"              6]
-    ["Toucan"                2]
-    ["Empty Vending Machine" 0]]])
+  [["number-of-cans"
+     [{:field-name "thing",          :base-type :type/Text}
+      {:field-name "number-of-cans", :base-type {:native "tinyint(1)"}}]
+     [["Six Pack"              6]
+      ["Toucan"                2]
+      ["Empty Vending Machine" 0]]]])
 
 (defn- db->fields [db]
   (let [table-ids (db/select-ids 'Table :db_id (u/get-id db))]
diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj
index 3280e3935c8aa6dc68ca3f5ab98cc3efb512b945..290140ecf7bf50fa0e68caaf83f712a7912e8049 100644
--- a/test/metabase/driver/postgres_test.clj
+++ b/test/metabase/driver/postgres_test.clj
@@ -75,13 +75,13 @@
 
 ;;; # UUID Support
 (i/def-database-definition ^:private with-uuid
-  ["users"
-   [{:field-name "user_id", :base-type :type/UUID}]
-   [[#uuid "4f01dcfd-13f7-430c-8e6f-e505c0851027"]
-    [#uuid "4652b2e7-d940-4d55-a971-7e484566663e"]
-    [#uuid "da1d6ecc-e775-4008-b366-c38e7a2e8433"]
-    [#uuid "7a5ce4a2-0958-46e7-9685-1a4eaa3bd08a"]
-    [#uuid "84ed434e-80b4-41cf-9c88-e334427104ae"]]])
+  [["users"
+     [{:field-name "user_id", :base-type :type/UUID}]
+     [[#uuid "4f01dcfd-13f7-430c-8e6f-e505c0851027"]
+      [#uuid "4652b2e7-d940-4d55-a971-7e484566663e"]
+      [#uuid "da1d6ecc-e775-4008-b366-c38e7a2e8433"]
+      [#uuid "7a5ce4a2-0958-46e7-9685-1a4eaa3bd08a"]
+      [#uuid "84ed434e-80b4-41cf-9c88-e334427104ae"]]]])
 
 
 ;; Check that we can load a Postgres Database with a :type/UUID
@@ -112,11 +112,11 @@
 
 ;; Make sure that Tables / Fields with dots in their names get escaped properly
 (i/def-database-definition ^:private dots-in-names
-  ["objects.stuff"
-   [{:field-name "dotted.name", :base-type :type/Text}]
-   [["toucan_cage"]
-    ["four_loko"]
-    ["ouija_board"]]])
+  [["objects.stuff"
+     [{:field-name "dotted.name", :base-type :type/Text}]
+     [["toucan_cage"]
+      ["four_loko"]
+      ["ouija_board"]]]])
 
 (expect-with-engine :postgres
   {:columns ["id" "dotted.name"]
@@ -130,14 +130,14 @@
 
 ;; Make sure that duplicate column names (e.g. caused by using a FK) still return both columns
 (i/def-database-definition ^:private duplicate-names
-  ["birds"
-   [{:field-name "name", :base-type :type/Text}]
-   [["Rasta"]
-    ["Lucky"]]]
-  ["people"
-   [{:field-name "name", :base-type :type/Text}
-    {:field-name "bird_id", :base-type :type/Integer, :fk :birds}]
-   [["Cam" 1]]])
+  [["birds"
+     [{:field-name "name", :base-type :type/Text}]
+     [["Rasta"]
+      ["Lucky"]]]
+   ["people"
+    [{:field-name "name", :base-type :type/Text}
+     {:field-name "bird_id", :base-type :type/Integer, :fk :birds}]
+    [["Cam" 1]]]])
 
 (expect-with-engine :postgres
   {:columns ["name" "name_2"]
@@ -150,10 +150,10 @@
 
 ;;; Check support for `inet` columns
 (i/def-database-definition ^:private ip-addresses
-  ["addresses"
-   [{:field-name "ip", :base-type {:native "inet"}}]
-   [[(hsql/raw "'192.168.1.1'::inet")]
-    [(hsql/raw "'10.4.4.15'::inet")]]])
+  [["addresses"
+     [{:field-name "ip", :base-type {:native "inet"}}]
+     [[(hsql/raw "'192.168.1.1'::inet")]
+      [(hsql/raw "'10.4.4.15'::inet")]]]])
 
 ;; Filtering by inet columns should add the appropriate SQL cast, e.g. `cast('192.168.1.1' AS inet)` (otherwise this
 ;; wouldn't work)
diff --git a/test/metabase/driver/sqlserver_test.clj b/test/metabase/driver/sqlserver_test.clj
index 21cb2c19916c08e5e417e2f9e57b25135cffde6f..75ac2428622a9b563cda18728ba81c485bc09789 100644
--- a/test/metabase/driver/sqlserver_test.clj
+++ b/test/metabase/driver/sqlserver_test.clj
@@ -18,9 +18,9 @@
   (apply str (repeatedly 1000 (partial rand-nth [\A \G \C \T]))))
 
 (def-database-definition ^:private ^:const genetic-data
-  ["genetic-data"
-   [{:field-name "gene", :base-type {:native "VARCHAR(MAX)"}}]
-   [[a-gene]]])
+  [["genetic-data"
+     [{:field-name "gene", :base-type {:native "VARCHAR(MAX)"}}]
+     [[a-gene]]]])
 
 (expect-with-engine :sqlserver
   [[1 a-gene]]
diff --git a/test/metabase/models/pulse_test.clj b/test/metabase/models/pulse_test.clj
index 600411d12da7a7a36589f778470ec4df642b9236..87224dafb67a77a55e7c6d9ddbb47d1f2236988a 100644
--- a/test/metabase/models/pulse_test.clj
+++ b/test/metabase/models/pulse_test.clj
@@ -51,7 +51,9 @@
    :name          "Lodi Dodi"
    :cards         [{:name        "Test Card"
                     :description nil
-                    :display     :table}]
+                    :display     :table
+                    :include_csv false
+                    :include_xls false}]
    :channels      [(merge pulse-channel-defaults
                           {:schedule_type  :daily
                            :schedule_hour  15
@@ -88,7 +90,7 @@
                   Card  [{card-id-2 :id} {:name "card2"}]
                   Card  [{card-id-3 :id} {:name "card3"}]]
     (let [upd-cards! (fn [cards]
-                       (update-pulse-cards! {:id pulse-id} cards)
+                       (update-pulse-cards! {:id pulse-id} (map create-card-ref cards))
                        (set (for [card-id (db/select-field :card_id PulseCard, :pulse_id pulse-id)]
                               (db/select-one-field :name Card, :id card-id))))]
       [(upd-cards! [])
@@ -128,19 +130,20 @@
                            :recipients     [{:email "foo@bar.com"}]})]
    :cards         [{:name        "Test Card"
                     :description nil
-                    :display     :table}]
+                    :display     :table
+                    :include_csv false
+                    :include_xls false}]
    :skip_if_empty false}
   (tt/with-temp Card [{:keys [id]} {:name "Test Card"}]
     (tu/with-model-cleanup [Pulse]
       (create-pulse-then-select! "Booyah!"
                                  (user->id :rasta)
-                                 [id]
+                                 [(create-card-ref id)]
                                  [{:channel_type  :email
                                    :schedule_type :daily
                                    :schedule_hour 18
                                    :recipients    [{:email "foo@bar.com"}]}]
                                  false))))
-
 ;; update-pulse!
 ;; basic update.  we are testing several things here
 ;;  1. ability to update the Pulse name
@@ -154,10 +157,14 @@
    :name          "We like to party"
    :cards         [{:name        "Bar Card"
                     :description nil
-                    :display     :bar}
+                    :display     :bar
+                    :include_csv false
+                    :include_xls false}
                    {:name        "Test Card"
                     :description nil
-                    :display     :table}]
+                    :display     :table
+                    :include_csv false
+                    :include_xls false}]
    :channels      [(merge pulse-channel-defaults
                           {:schedule_type  :daily
                            :schedule_hour  18
@@ -171,7 +178,7 @@
     (update-pulse-then-select! {:id             pulse-id
                                 :name           "We like to party"
                                 :creator_id     (user->id :crowberto)
-                                :cards          [card-id-2 card-id-1]
+                                :cards          (mapv create-card-ref [card-id-2 card-id-1])
                                 :channels       [{:channel_type  :email
                                                   :schedule_type :daily
                                                   :schedule_hour 18
diff --git a/test/metabase/permissions_test.clj b/test/metabase/permissions_test.clj
index 20734d52e2d7f423d80039ffb54f911e1d3de5fb..7acd179c39e89378d8671e363eef8c99a1d3beb4 100644
--- a/test/metabase/permissions_test.clj
+++ b/test/metabase/permissions_test.clj
@@ -19,9 +19,10 @@
              [segment :refer [Segment]]
              [table :refer [Table]]]
             [metabase.query-processor.middleware.expand :as ql]
-            [metabase.test.data :as data]
+            [metabase.test
+             [data :as data]
+             [util :as tu]]
             [metabase.test.data.users :as test-users]
-            [metabase.test.util :as tu]
             [metabase.util :as u]
             [toucan.db :as db]
             [toucan.util.test :as tt])
diff --git a/test/metabase/pulse_test.clj b/test/metabase/pulse_test.clj
index 7534a60bae5aeb1c2630e9f698eb4ead219110f6..7c4b06ecfaeddabe54957b1af6a5cd3758befd7e 100644
--- a/test/metabase/pulse_test.clj
+++ b/test/metabase/pulse_test.clj
@@ -541,3 +541,110 @@
      (send-pulse! (retrieve-pulse-or-alert pulse-id))
      [@et/inbox
       (db/exists? Pulse :id pulse-id)])))
+
+(def ^:private csv-attachment
+  {:type :attachment, :content-type "text/csv", :file-name "Test card.csv",
+   :content java.net.URL, :description "Full results for 'Test card'", :content-id false})
+
+(def ^:private xls-attachment
+  {:type :attachment, :file-name "Test card.xlsx",
+   :content-type "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+   :content java.net.URL, :description "Full results for 'Test card'", :content-id false})
+
+(defn- add-rasta-attachment
+  "Append `ATTACHMENT` to the first email found for Rasta"
+  [email attachment]
+  (update-in email ["rasta@metabase.com" 0] #(update % :body conj attachment)))
+
+;; Basic test, 1 card, 1 recipient, with CSV attachment
+(expect
+  (add-rasta-attachment (rasta-pulse-email) csv-attachment)
+
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                [{pulse-id :id} {:name          "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id    pulse-id
+                                                        :card_id     card-id
+                                                        :position    0
+                                                        :include_csv true}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
+
+;; Basic alert test, 1 card, 1 recipient, with CSV attachment
+(expect
+  (rasta-alert-email "Metabase alert: Test card has results"
+                     [{"Test card.*has results for you to see" true}, png-attachment, csv-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                         :alert_first_only false}]
+                  PulseCard             [_              {:pulse_id    pulse-id
+                                                         :card_id     card-id
+                                                         :position    0
+                                                         :include_csv true}]
+                  PulseChannel          [{pc-id :id}    {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_              {:user_id          (rasta-id)
+                                                         :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has results for you to see"))))
+
+;; Basic test of card with CSV and XLS attachments, but no data. Should not include an attachment
+(expect
+  (rasta-pulse-email)
+
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:filter   [">",["field-id" (data/id :checkins :date)],"2017-10-24"]
+                                                                        :breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                [{pulse-id :id} {:name          "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id    pulse-id
+                                                        :card_id     card-id
+                                                        :position    0
+                                                        :include_csv true
+                                                        :include_xls true}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
+
+;; Basic test, 1 card, 1 recipient, with XLS attachment
+(expect
+  (add-rasta-attachment (rasta-pulse-email) xls-attachment)
+
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                [{pulse-id :id} {:name          "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id    pulse-id
+                                                        :card_id     card-id
+                                                        :position    0
+                                                        :include_xls true}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"))))
+
+;; Rows alert with data and a CSV + XLS attachment
+(expect
+  (rasta-alert-email "Metabase alert: Test card has results"
+                     [{"Test card.*has results for you to see" true}, png-attachment, csv-attachment, xls-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                         :alert_first_only false}]
+                  PulseCard             [_             {:pulse_id    pulse-id
+                                                        :card_id     card-id
+                                                        :position    0
+                                                        :include_csv true
+                                                        :include_xls true}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has results for you to see"))))
diff --git a/test/metabase/query_processor/middleware/format_rows_test.clj b/test/metabase/query_processor/middleware/format_rows_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..c504eb58451e0ed74109ab708db8f4834a46cd83
--- /dev/null
+++ b/test/metabase/query_processor/middleware/format_rows_test.clj
@@ -0,0 +1,57 @@
+(ns metabase.query-processor.middleware.format-rows-test
+  (:require [metabase.query-processor-test :as qpt]
+            [metabase.query-processor.middleware.expand :as ql]
+            [metabase.test
+             [data :as data]
+             [util :as tu]]
+            [metabase.test.data
+             [dataset-definitions :as defs]
+             [datasets :refer [*engine*]]]))
+
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift :presto}
+  (if (= :sqlite *engine*)
+    [[1 "Plato Yeshua" "2014-04-01 00:00:00" "08:30:00"]
+     [2 "Felipinho Asklepios" "2014-12-05 00:00:00" "15:15:00"]
+     [3 "Kaneonuskatew Eiran" "2014-11-06 00:00:00" "16:15:00"]
+     [4 "Simcha Yan" "2014-01-01 00:00:00" "08:30:00"]
+     [5 "Quentin Sören" "2014-10-03 00:00:00" "17:30:00"]]
+
+    [[1 "Plato Yeshua" "2014-04-01T00:00:00.000Z" "08:30:00.000Z"]
+     [2 "Felipinho Asklepios" "2014-12-05T00:00:00.000Z" "15:15:00.000Z"]
+     [3 "Kaneonuskatew Eiran" "2014-11-06T00:00:00.000Z" "16:15:00.000Z"]
+     [4 "Simcha Yan" "2014-01-01T00:00:00.000Z" "08:30:00.000Z"]
+     [5 "Quentin Sören" "2014-10-03T00:00:00.000Z" "17:30:00.000Z"]])
+  (->> (data/with-db (data/get-or-create-database! defs/test-data-with-time)
+         (data/run-query users
+           (ql/order-by (ql/asc $id))
+           (ql/limit 5)))
+       qpt/rows))
+
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift :presto}
+  (cond
+    (= :sqlite *engine*)
+    [[1 "Plato Yeshua" "2014-04-01 00:00:00" "08:30:00"]
+     [2 "Felipinho Asklepios" "2014-12-05 00:00:00" "15:15:00"]
+     [3 "Kaneonuskatew Eiran" "2014-11-06 00:00:00" "16:15:00"]
+     [4 "Simcha Yan" "2014-01-01 00:00:00" "08:30:00"]
+     [5 "Quentin Sören" "2014-10-03 00:00:00" "17:30:00"]]
+
+    (qpt/supports-report-timezone? *engine*)
+    [[1 "Plato Yeshua" "2014-04-01T00:00:00.000-07:00" "00:30:00.000-08:00"]
+     [2 "Felipinho Asklepios" "2014-12-05T00:00:00.000-08:00" "07:15:00.000-08:00"]
+     [3 "Kaneonuskatew Eiran" "2014-11-06T00:00:00.000-08:00" "08:15:00.000-08:00"]
+     [4 "Simcha Yan" "2014-01-01T00:00:00.000-08:00" "00:30:00.000-08:00"]
+     [5 "Quentin Sören" "2014-10-03T00:00:00.000-07:00" "09:30:00.000-08:00"]]
+
+    :else
+    [[1 "Plato Yeshua" "2014-04-01T00:00:00.000Z" "08:30:00.000Z"]
+     [2 "Felipinho Asklepios" "2014-12-05T00:00:00.000Z" "15:15:00.000Z"]
+     [3 "Kaneonuskatew Eiran" "2014-11-06T00:00:00.000Z" "16:15:00.000Z"]
+     [4 "Simcha Yan" "2014-01-01T00:00:00.000Z" "08:30:00.000Z"]
+     [5 "Quentin Sören" "2014-10-03T00:00:00.000Z" "17:30:00.000Z"]])
+  (tu/with-temporary-setting-values [report-timezone "America/Los_Angeles"]
+    (->> (data/with-db (data/get-or-create-database! defs/test-data-with-time)
+           (data/run-query users
+             (ql/order-by (ql/asc $id))
+             (ql/limit 5)))
+         qpt/rows)))
diff --git a/test/metabase/query_processor/middleware/parameters/date_test.clj b/test/metabase/query_processor/middleware/parameters/date_test.clj
index 4012819d7b9d105f3ed092a4ab8945555b18b798..5fac33a70ff7d230344d5b527c5c5b4a1dbe8a1e 100644
--- a/test/metabase/query_processor/middleware/parameters/date_test.clj
+++ b/test/metabase/query_processor/middleware/parameters/date_test.clj
@@ -1,12 +1,13 @@
 (ns metabase.query-processor.middleware.parameters.date-test
+  "Tests for datetime parameters."
   (:require [expectations :refer :all]
             [clj-time.core :as t]
             [metabase.query-processor.middleware.parameters.dates :refer :all]))
 
 ;; we hard code "now" to a specific point in time so that we can control the test output
 (defn- test-date->range [value]
-  (with-redefs-fn {#'clj-time.core/now (fn [] (t/date-time 2016 06 07 12 0 0))}
-    #(date-string->range value nil)))
+  (with-redefs [t/now (constantly (t/date-time 2016 06 07 12 0 0))]
+    (date-string->range value nil)))
 
 (expect {:end "2016-03-31", :start "2016-01-01"} (test-date->range "Q1-2016"))
 (expect {:end "2016-02-29", :start "2016-02-01"} (test-date->range "2016-02"))
diff --git a/test/metabase/query_processor/middleware/parameters/mbql_test.clj b/test/metabase/query_processor/middleware/parameters/mbql_test.clj
index 6ff5b8715462bfe285644897ff7fcaec37e65aa2..4d9c72018f29744453e575f3cb2858cd37ba010f 100644
--- a/test/metabase/query_processor/middleware/parameters/mbql_test.clj
+++ b/test/metabase/query_processor/middleware/parameters/mbql_test.clj
@@ -1,14 +1,20 @@
 (ns metabase.query-processor.middleware.parameters.mbql-test
   "Tests for *MBQL* parameter substitution."
   (:require [expectations :refer :all]
+            [honeysql.core :as hsql]
             [metabase
              [query-processor :as qp]
-             [query-processor-test :refer [first-row rows format-rows-by non-timeseries-engines]]]
+             [query-processor-test :refer [first-row format-rows-by non-timeseries-engines rows]]
+             [util :as u]]
+            [metabase.driver.generic-sql :as sql]
+            [metabase.models
+             [field :refer [Field]]
+             [table :refer [Table]]]
             [metabase.query-processor.middleware.expand :as ql]
             [metabase.query-processor.middleware.parameters.mbql :refer :all]
             [metabase.test.data :as data]
             [metabase.test.data.datasets :as datasets]
-            [metabase.util :as u]))
+            [metabase.util.honeysql-extensions :as hx]))
 
 (defn- expand-parameters [query]
   (expand (dissoc query :parameters) (:parameters query)))
@@ -203,8 +209,12 @@
       (rows (qp/process-query outer-query)))))
 
 ;; now let's make sure the correct query is actually being generated for the same thing above...
-(datasets/expect-with-engines params-test-engines
-  {:query  (str "SELECT count(*) AS \"count\" FROM \"PUBLIC\".\"VENUES\" "
+;; (NOTE: We're only testing this with H2 because the SQL generated is simply too different between various SQL drivers.
+;; we know the features are still working correctly because we're actually checking that we get the right result from
+;; running the query above these tests are more of a sanity check to make sure the SQL generated is sane.)
+(datasets/expect-with-engine :h2
+  {:query  (str "SELECT count(*) AS \"count\" "
+                "FROM \"PUBLIC\".\"VENUES\" "
                 "WHERE (\"PUBLIC\".\"VENUES\".\"PRICE\" = 3 OR \"PUBLIC\".\"VENUES\".\"PRICE\" = 4)")
    :params nil}
   (let [inner-query (data/query venues
@@ -219,7 +229,7 @@
 
 ;; try it with date params as well. Even though there's no way to do this in the frontend AFAIK there's no reason we
 ;; can't handle it on the backend
-(datasets/expect-with-engines params-test-engines
+(datasets/expect-with-engine :h2
   {:query  (str "SELECT count(*) AS \"count\" FROM \"PUBLIC\".\"CHECKINS\" "
                 "WHERE (CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN CAST(? AS date) AND CAST(? AS date) "
                 "OR CAST(\"PUBLIC\".\"CHECKINS\".\"DATE\" AS date) BETWEEN CAST(? AS date) AND CAST(? AS date))")
diff --git a/test/metabase/query_processor/middleware/parameters/sql_test.clj b/test/metabase/query_processor/middleware/parameters/sql_test.clj
index ddf96ef6596a9114627ae6c618ce287323ecc825..19caca4dde770d2fb273adbbc45a497e45892a10 100644
--- a/test/metabase/query_processor/middleware/parameters/sql_test.clj
+++ b/test/metabase/query_processor/middleware/parameters/sql_test.clj
@@ -292,7 +292,7 @@
 ;;; ------------------------------------------ expansion tests: dimensions -------------------------------------------
 
 (defn- expand-with-dimension-param [dimension-param]
-  (with-redefs [t/now (fn [] (t/date-time 2016 06 07 12 0 0))]
+  (with-redefs [t/now (constantly (t/date-time 2016 06 07 12 0 0))]
     (expand* {:native     {:query "SELECT * FROM checkins WHERE {{date}};"
                            :template_tags {:date {:name "date", :display_name "Checkin Date", :type "dimension", :dimension ["field-id" (data/id :checkins :date)]}}}
               :parameters (when dimension-param
@@ -563,7 +563,7 @@
                                   :dimension    ["field-id" (data/id :checkins :date)]}}
    :params        [#inst "2017-10-31T00:00:00.000000000-00:00"
                    #inst "2017-11-04T00:00:00.000000000-00:00"]}
-  (with-redefs [t/now (fn [] (t/date-time 2017 11 05 12 0 0))]
+  (with-redefs [t/now (constantly (t/date-time 2017 11 05 12 0 0))]
     (:native (expand {:driver     (driver/engine->driver :h2)
                       :native     {:query         (str "SELECT count(*) AS \"count\", \"DATE\" "
                                                        "FROM CHECKINS "
@@ -607,7 +607,7 @@
                     :widget_type  "date/all-options"}}
    :params        [#inst "2017-10-31T00:00:00.000000000-00:00"
                    #inst "2017-11-04T00:00:00.000000000-00:00"]}
-  (with-redefs [t/now (fn [] (t/date-time 2017 11 05 12 0 0))]
+  (with-redefs [t/now (constantly (t/date-time 2017 11 05 12 0 0))]
     (:native (expand {:driver (driver/engine->driver :h2)
                       :native {:query         (str "SELECT count(*) AS \"count\", \"DATE\" "
                                                    "FROM CHECKINS "
diff --git a/test/metabase/query_processor_test/expressions_test.clj b/test/metabase/query_processor_test/expressions_test.clj
index d39a64a1742165bce60e0a7951ef4cf1c862c1e1..2b19acf4f2707465489f7a2d84ceeb50a1e6bfb6 100644
--- a/test/metabase/query_processor_test/expressions_test.clj
+++ b/test/metabase/query_processor_test/expressions_test.clj
@@ -97,3 +97,15 @@
             (ql/expressions {:x (ql/* $price 2.0)})
             (ql/aggregation (ql/count))
             (ql/breakout (ql/expression :x))))))
+
+;; Custom aggregation expressions should include their type
+(datasets/expect-with-engines (engines-that-support :expressions)
+  (conj #{{:name "x" :base_type :type/Float}}
+        (if (= datasets/*engine* :oracle)
+          {:name (data/format-name "category_id") :base_type :type/Decimal}
+          {:name (data/format-name "category_id") :base_type :type/Integer}))
+  (set (map #(select-keys % [:name :base_type])
+            (-> (data/run-query venues
+                  (ql/aggregation (ql/named (ql/sum (ql/* $price -1)) "x"))
+                  (ql/breakout $category_id))
+                (get-in [:data :cols])))))
diff --git a/test/metabase/query_processor_test/field_visibility_test.clj b/test/metabase/query_processor_test/field_visibility_test.clj
index fe7a78423b57c56f4d09ae937b9c0b7043ff0ba4..7b872e5b2b128697bc5ee6ad1dd8d42993613540 100644
--- a/test/metabase/query_processor_test/field_visibility_test.clj
+++ b/test/metabase/query_processor_test/field_visibility_test.clj
@@ -10,7 +10,8 @@
              [util :as tu]]
             [toucan.db :as db]))
 
-;;; ------------------------------------------------------------ :details-only fields  ------------------------------------------------------------
+;;; ---------------------------------------------- :details-only fields ----------------------------------------------
+
 ;; make sure that rows where visibility_type = details-only are included and properly marked up
 (defn- get-col-names []
   (-> (data/run-query venues
@@ -36,7 +37,8 @@
        (get-col-names))])
 
 
-;;; ------------------------------------------------------------ :sensitive fields ------------------------------------------------------------
+;;; ----------------------------------------------- :sensitive fields ------------------------------------------------
+
 ;;; Make sure :sensitive information fields are never returned by the QP
 (qp-expect-with-all-engines
   {:columns     (->columns "id" "name" "last_login")
diff --git a/test/metabase/query_processor_test/time_field_test.clj b/test/metabase/query_processor_test/time_field_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..688b30b7b061d94d6c0578aefb3a301ab77d4034
--- /dev/null
+++ b/test/metabase/query_processor_test/time_field_test.clj
@@ -0,0 +1,102 @@
+(ns metabase.query-processor-test.time-field-test
+  (:require [metabase.query-processor-test :as qpt]
+            [metabase.query-processor.middleware.expand :as ql]
+            [metabase.test
+             [data :as data]
+             [util :as tu]]
+            [metabase.test.data
+             [dataset-definitions :as defs]
+             [datasets :refer [*engine*]]]))
+
+(defmacro ^:private time-query [& filter-clauses]
+  `(qpt/rows
+     (data/with-db (data/get-or-create-database! defs/test-data-with-time)
+       (data/run-query users
+         (ql/fields ~'$id ~'$name ~'$last_login_time)
+         (ql/order-by (ql/asc ~'$id))
+         ~@filter-clauses))))
+
+;; Basic between query on a time field
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift}
+  (if (= :sqlite *engine*)
+    [[1 "Plato Yeshua" "08:30:00"]
+     [4 "Simcha Yan"   "08:30:00"]]
+
+    [[1 "Plato Yeshua" "08:30:00.000Z"]
+     [4 "Simcha Yan"   "08:30:00.000Z"]])
+  (time-query (ql/filter (ql/between $last_login_time
+                                     "08:00:00"
+                                     "09:00:00"))))
+
+;; Basic between query on a time field with milliseconds
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift}
+  (if (= :sqlite *engine*)
+    [[1 "Plato Yeshua" "08:30:00"]
+     [4 "Simcha Yan"   "08:30:00"]]
+
+    [[1 "Plato Yeshua" "08:30:00.000Z"]
+     [4 "Simcha Yan"   "08:30:00.000Z"]])
+  (time-query (ql/filter (ql/between $last_login_time
+                                     "08:00:00.000"
+                                     "09:00:00.000"))))
+
+;; Basic > query with a time field
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift}
+  (if (= :sqlite *engine*)
+    [[3 "Kaneonuskatew Eiran" "16:15:00"]
+     [5 "Quentin Sören" "17:30:00"]
+     [10 "Frans Hevel" "19:30:00"]]
+
+    [[3 "Kaneonuskatew Eiran" "16:15:00.000Z"]
+     [5 "Quentin Sören" "17:30:00.000Z"]
+     [10 "Frans Hevel" "19:30:00.000Z"]])
+  (time-query (ql/filter (ql/> $last_login_time "16:00:00.000Z"))))
+
+;; Basic query with an = filter on a time field
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift}
+  (if (= :sqlite *engine*)
+    [[3 "Kaneonuskatew Eiran" "16:15:00"]]
+
+    [[3 "Kaneonuskatew Eiran" "16:15:00.000Z"]])
+  (time-query (ql/filter (ql/= $last_login_time "16:15:00.000Z"))))
+
+;; Query with a time filter and a report timezone
+(qpt/expect-with-non-timeseries-dbs-except #{:oracle :mongo :redshift}
+  (cond
+    (= :sqlite *engine*)
+    [[1 "Plato Yeshua" "08:30:00"]
+     [4 "Simcha Yan" "08:30:00"]]
+
+    ;; This is the correct "answer" to this query, though it doesn't
+    ;; pass through JDBC. The 08:00 is adjusted to UTC (16:00), which
+    ;; should yield the third item
+    (= :presto *engine*)
+    [[3 "Kaneonuskatew Eiran" "00:15:00.000-08:00"]]
+
+    ;; Best I can tell, MySQL's interpretation of this part of the
+    ;; JDBC is way off. This doesn't return results because it looks
+    ;; like it's basically double converting the time to
+    ;; America/Los_Angeles. What's getting sent to the database is
+    ;; 00:00 and 01:00 (which we have no data in that range). I think
+    ;; we'll need to switch to their new JDBC date code to get this
+    ;; fixed
+    (= :mysql *engine*)
+    []
+
+    ;; Databases like PostgreSQL ignore timezone information when
+    ;; using a time field, the result below is what happens when the
+    ;; 08:00 time is interpreted as UTC, then not adjusted to Pacific
+    ;; time by the DB
+    (qpt/supports-report-timezone? *engine*)
+    [[1 "Plato Yeshua" "00:30:00.000-08:00"]
+     [4 "Simcha Yan" "00:30:00.000-08:00"]]
+
+    :else
+    [[1 "Plato Yeshua" "08:30:00.000Z"]
+     [4 "Simcha Yan" "08:30:00.000Z"]])
+  (tu/with-temporary-setting-values [report-timezone "America/Los_Angeles"]
+    (time-query (ql/filter (apply ql/between
+                                  $last_login_time
+                                  (if (qpt/supports-report-timezone? *engine*)
+                                    ["08:00:00" "09:00:00"]
+                                    ["08:00:00-00:00" "09:00:00-00:00"]))))))
diff --git a/test/metabase/sync/sync_metadata/metabase_metadata_test.clj b/test/metabase/sync/sync_metadata/metabase_metadata_test.clj
index aa8c465f06c3db2faa256043e3348368647fc723..3d2b617106d0de1fd3d0d7d5abdab810dc492450 100644
--- a/test/metabase/sync/sync_metadata/metabase_metadata_test.clj
+++ b/test/metabase/sync/sync_metadata/metabase_metadata_test.clj
@@ -3,6 +3,7 @@
   (:require [expectations :refer :all]
             [metabase.models
              [database :refer [Database]]
+             [field :refer [Field]]
              [table :refer [Table]]]
             [metabase.sync.sync-metadata.metabase-metadata :as metabase-metadata]
             [metabase.test.util :as tu]
@@ -10,8 +11,7 @@
             [toucan
              [db :as db]
              [hydrate :refer [hydrate]]]
-            [toucan.util.test :as tt]
-            [metabase.models.field :refer [Field]]))
+            [toucan.util.test :as tt]))
 
 ;; Test that the `_metabase_metadata` table can be used to populate values for things like descriptions
 (defn- get-table-and-fields-descriptions [table-or-id]
diff --git a/test/metabase/sync/sync_metadata/tables_test.clj b/test/metabase/sync/sync_metadata/tables_test.clj
index 51d73cd46d78ee784f199a525bd2bfeebcb3c286..73f85d9cd58d4e2fc40526498a7ead9c5cf49cfd 100644
--- a/test/metabase/sync/sync_metadata/tables_test.clj
+++ b/test/metabase/sync/sync_metadata/tables_test.clj
@@ -7,18 +7,18 @@
             [toucan.db :as db]))
 
 (i/def-database-definition ^:const ^:private db-with-some-cruft
-  ["acquired_toucans"
-   [{:field-name "species",              :base-type :type/Text}
-    {:field-name "cam_has_acquired_one", :base-type :type/Boolean}]
-   [["Toco"               false]
-    ["Chestnut-Mandibled" true]
-    ["Keel-billed"        false]
-    ["Channel-billed"     false]]]
-  ["south_migrationhistory"
-   [{:field-name "app_name",  :base-type :type/Text}
-    {:field-name "migration", :base-type :type/Text}]
-   [["main" "0001_initial"]
-    ["main" "0002_add_toucans"]]])
+  [["acquired_toucans"
+     [{:field-name "species",              :base-type :type/Text}
+      {:field-name "cam_has_acquired_one", :base-type :type/Boolean}]
+     [["Toco"               false]
+      ["Chestnut-Mandibled" true]
+      ["Keel-billed"        false]
+      ["Channel-billed"     false]]]
+   ["south_migrationhistory"
+    [{:field-name "app_name",  :base-type :type/Text}
+     {:field-name "migration", :base-type :type/Text}]
+    [["main" "0001_initial"]
+     ["main" "0002_add_toucans"]]]])
 
 ;; south_migrationhistory, being a CRUFTY table, should still be synced, but marked as such
 (expect
diff --git a/test/metabase/test/data/bigquery.clj b/test/metabase/test/data/bigquery.clj
index ededdbea25da16b291032a6d91df9a5481d8d623..b6318d19b17c1db00e83f68fdb62c7af3d80df7f 100644
--- a/test/metabase/test/data/bigquery.clj
+++ b/test/metabase/test/data/bigquery.clj
@@ -1,5 +1,8 @@
 (ns metabase.test.data.bigquery
-  (:require [clojure.string :as str]
+  (:require [clj-time
+             [coerce :as tcoerce]
+             [format :as tformat]]
+            [clojure.string :as str]
             [medley.core :as m]
             [metabase.driver
              [bigquery :as bigquery]
@@ -12,9 +15,8 @@
             [schema.core :as s])
   (:import com.google.api.client.util.DateTime
            com.google.api.services.bigquery.Bigquery
-           [com.google.api.services.bigquery.model Dataset DatasetReference QueryRequest Table
-            TableDataInsertAllRequest TableDataInsertAllRequest$Rows TableFieldSchema TableReference TableRow
-            TableSchema]
+           [com.google.api.services.bigquery.model Dataset DatasetReference QueryRequest Table TableDataInsertAllRequest TableDataInsertAllRequest$Rows TableFieldSchema TableReference TableRow TableSchema]
+           java.sql.Time
            metabase.driver.bigquery.BigQueryDriver))
 
 ;;; ----------------------------------------------- Connection Details -----------------------------------------------
@@ -57,7 +59,7 @@
   (println (u/format-color 'red "Deleted BigQuery dataset '%s'." dataset-id)))
 
 (def ^:private ^:const valid-field-types
-  #{:BOOLEAN :FLOAT :INTEGER :RECORD :STRING :TIMESTAMP})
+  #{:BOOLEAN :FLOAT :INTEGER :RECORD :STRING :TIMESTAMP :TIME})
 
 (s/defn ^:private create-table!
   [dataset-id       :- su/NonBlankString
@@ -102,9 +104,10 @@
                                 (.setRows (for [row-map row-maps]
                                             (let [data (TableRow.)]
                                               (doseq [[k v] row-map
-                                                      :let [v (if (instance? honeysql.types.SqlCall v)
+                                                      :let [v (cond
+                                                                (instance? honeysql.types.SqlCall v)
                                                                 (timestamp-honeysql-form->GoogleDateTime v)
-                                                                v)]]
+                                                                :else v)]]
                                                 (.set data (name k) v))
                                               (doto (TableDataInsertAllRequest$Rows.)
                                                 (.setJson data))))))))
@@ -135,7 +138,7 @@
    :type/Float      :FLOAT
    :type/Integer    :INTEGER
    :type/Text       :STRING
-   :type/Time       :TIMESTAMP})
+   :type/Time       :TIME})
 
 (defn- fielddefs->field-name->base-type
   "Convert FIELD-DEFINITIONS to a format appropriate for passing to `create-table!`."
@@ -147,6 +150,14 @@
                      (println (u/format-color 'red "Don't know what BigQuery type to use for base type: %s" base-type))
                      (throw (Exception. (format "Don't know what BigQuery type to use for base type: %s" base-type))))})))
 
+(defn- time->string
+  "Coerces `T` to a Joda DateTime object and returns it's String
+  representation."
+  [t]
+  (->> t
+       tcoerce/to-date-time
+       (tformat/unparse #'bigquery/bigquery-time-format)))
+
 (defn- tabledef->prepared-rows
   "Convert TABLE-DEFINITION to a format approprate for passing to `insert-data!`."
   [{:keys [field-definitions rows]}]
@@ -154,11 +165,17 @@
   (let [field-names (map :field-name field-definitions)]
     (for [[i row] (m/indexed rows)]
       (assoc (zipmap field-names (for [v row]
-                                   (u/prog1 (if (instance? java.util.Date v)
+                                   (u/prog1 (cond
+
+                                              (instance? Time v)
+                                              (time->string v)
+
+                                              (instance? java.util.Date v)
                                               ;; convert to Google version of DateTime, otherwise it doesn't work (!)
                                               (DateTime. ^java.util.Date v)
-                                              v)
-                                            (assert (some? <>))))) ; make sure v is non-nil
+
+                                              :else v)
+                                            (assert (not (nil? <>)))))) ; make sure v is non-nil
              :id (inc i)))))
 
 (defn- load-tabledef! [dataset-name {:keys [table-name field-definitions], :as tabledef}]
diff --git a/test/metabase/test/data/dataset_definitions.clj b/test/metabase/test/data/dataset_definitions.clj
index 255b1bb208eaf5316153f37a677522adee27fa92..d8b7b80e925e7f0202419a7336bae03f236cf6d1 100644
--- a/test/metabase/test/data/dataset_definitions.clj
+++ b/test/metabase/test/data/dataset_definitions.clj
@@ -1,36 +1,72 @@
 (ns metabase.test.data.dataset-definitions
   "Definitions of various datasets for use in tests with `with-temp-db`."
   (:require [clojure.tools.reader.edn :as edn]
-            [metabase.test.data.interface :refer [def-database-definition]]))
-
+            [metabase.test.data.interface :as di])
+  (:import java.sql.Time
+           java.util.Calendar))
 
 ;; ## Datasets
 
-(def ^:private ^:const edn-definitions-dir "./test/metabase/test/data/dataset_definitions/")
-
-;; TODO - move this to interface
-;; TODO - make rows be lazily loadable for DB definitions from a file
-(defmacro ^:private def-database-definition-edn [dbname]
-  `(def-database-definition ~dbname
-     ~@(edn/read-string (slurp (str edn-definitions-dir (name dbname) ".edn")))))
-
 ;; The O.G. "Test Database" dataset
-(def-database-definition-edn test-data)
+(di/def-database-definition-edn test-data)
 
 ;; Times when the Toucan cried
-(def-database-definition-edn sad-toucan-incidents)
+(di/def-database-definition-edn sad-toucan-incidents)
 
 ;; Places, times, and circumstances where Tupac was sighted
-(def-database-definition-edn tupac-sightings)
+(di/def-database-definition-edn tupac-sightings)
 
-(def-database-definition-edn geographical-tips)
+(di/def-database-definition-edn geographical-tips)
 
 ;; A very tiny dataset with a list of places and a booleans
-(def-database-definition-edn places-cam-likes)
+(di/def-database-definition-edn places-cam-likes)
 
 ;; A small dataset with users and a set of messages between them. Each message has *2* foreign keys to user --
 ;; sender and reciever -- allowing us to test situations where multiple joins for a *single* table should occur.
-(def-database-definition-edn avian-singles)
+(di/def-database-definition-edn avian-singles)
+
+(defn- date-only
+  "This function emulates a date only field as it would come from the
+  JDBC driver. The hour/minute/second/millisecond fields should be 0s"
+  [date]
+  (let [orig-cal (doto (Calendar/getInstance)
+                   (.setTime date))]
+    (-> (doto (Calendar/getInstance)
+          (.clear)
+          (.set Calendar/YEAR (.get orig-cal Calendar/YEAR))
+          (.set Calendar/MONTH (.get orig-cal Calendar/MONTH))
+          (.set Calendar/DAY_OF_MONTH (.get orig-cal Calendar/DAY_OF_MONTH)))
+        .getTime)))
+
+(defn- time-only
+  "This function will return a java.sql.Time object. To create a Time
+  object similar to what JDBC would return, the time needs to be
+  relative to epoch. As an example a time of 4:30 would be a Time
+  instance, but it's a subclass of Date, so it looks like
+  1970-01-01T04:30:00.000"
+  [date]
+  (let [orig-cal (doto (Calendar/getInstance)
+                   (.setTime date))]
+    (-> (doto (Calendar/getInstance)
+          (.clear)
+          (.set Calendar/HOUR_OF_DAY (.get orig-cal Calendar/HOUR_OF_DAY))
+          (.set Calendar/MINUTE (.get orig-cal Calendar/MINUTE))
+          (.set Calendar/SECOND (.get orig-cal Calendar/SECOND)))
+        .getTimeInMillis
+        Time.)))
+
+(di/def-database-definition test-data-with-time
+  (di/update-table-def "users"
+                       (fn [table-def]
+                         [(first table-def)
+                          {:field-name "last_login_date", :base-type :type/Date}
+                          {:field-name "last_login_time", :base-type :type/Time}
+                          (peek table-def)])
+                       (fn [rows]
+                         (mapv (fn [[username last-login password-text]]
+                                 [username (date-only last-login) (time-only last-login) password-text])
+                               rows))
+                       (di/slurp-edn-table-def "test-data")))
 
 (def test-data-map
   "Converts data from `test-data` to a map of maps like the following:
diff --git a/test/metabase/test/data/datasets.clj b/test/metabase/test/data/datasets.clj
index 328581e881063f28bb6b9cc2587249b005a1140c..0cb4095c124bba2d534736e2d9be739049349e0b 100644
--- a/test/metabase/test/data/datasets.clj
+++ b/test/metabase/test/data/datasets.clj
@@ -88,6 +88,7 @@
   "Bind `*engine*` and `*driver*` as appropriate for ENGINE and execute F, a function that takes no args."
   {:style/indent 1}
   [engine f]
+  {:pre [(keyword? engine)]}
   (binding [*engine* engine
             *driver* (engine->driver engine)]
     (f)))
diff --git a/test/metabase/test/data/generic_sql.clj b/test/metabase/test/data/generic_sql.clj
index 7e823cbc49122f612603c968f2e865972be27b70..a4fcbcf6d76f6f5f1f30feae4fb05bf87bf608a4 100644
--- a/test/metabase/test/data/generic_sql.clj
+++ b/test/metabase/test/data/generic_sql.clj
@@ -66,6 +66,7 @@
  By default, this qualifies field names with their table name, but otherwise does no other specific
  qualification.")
 
+  ;; TODO - why can't we just use `honeysql.core/format` with the `:quoting` options set to the driver's `quote-style`?
   (quote-name ^String [this, ^String nm]
     "*Optional*. Quote a name. Defaults to using double quotes.")
 
@@ -142,6 +143,7 @@
                (name (hx/qualify-and-escape-dots (quote-name driver n))))))
 
 (defn- default-qualify+quote-name
+  ;; TODO - what about schemas?
   ([driver db-name]
    (quote+combine-names driver (qualified-name-components driver db-name)))
   ([driver db-name table-name]
@@ -169,7 +171,8 @@
                                 (:field-definitions tabledef))]
     (for [row (:rows tabledef)]
       (zipmap fields-for-insert (for [v row]
-                                  (if (instance? java.util.Date v)
+                                  (if (and (not (instance? java.sql.Time v))
+                                           (instance? java.util.Date v))
                                     (u/->Timestamp v)
                                     v))))))
 
diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj
index c871fa0cc66204ba6cf306e343fd0701fb1f53ae..36d3584dea3f7c1c39786a523d49bf660ba456b3 100644
--- a/test/metabase/test/data/interface.clj
+++ b/test/metabase/test/data/interface.clj
@@ -4,6 +4,7 @@
    Objects that implement `IDriverTestExtensions` know how to load a `DatabaseDefinition` into an
    actual physical RDMS database. This functionality allows us to easily test with multiple datasets."
   (:require [clojure.string :as str]
+            [clojure.tools.reader.edn :as edn]
             [environ.core :refer [env]]
             [metabase
              [db :as db]
@@ -166,15 +167,36 @@
                                                            :table-definitions (mapv (partial apply create-table-definition)
                                                                                     table-name+field-definition-maps+rows)})))
 
+(def ^:private ^:const edn-definitions-dir "./test/metabase/test/data/dataset_definitions/")
+
+(defn slurp-edn-table-def [dbname]
+  (edn/read-string (slurp (str edn-definitions-dir dbname ".edn"))))
+
+(defn update-table-def
+  "Function useful for modifying a table definition before it's
+  applied. Will invoke `UPDATE-TABLE-DEF-FN` on the vector of column
+  definitions and `UPDATE-ROWS-FN` with the vector of rows in the
+  database definition. `TABLE-DEF` is the database
+  definition (typically used directly in a `def-database-definition`
+  invocation)."
+  [table-name-to-update update-table-def-fn update-rows-fn table-def]
+  (vec
+   (for [[table-name table-def rows] table-def
+         :when (= table-name table-name-to-update)]
+     [table-name
+      (update-table-def-fn table-def)
+      (update-rows-fn rows)])))
+
 (defmacro def-database-definition
   "Convenience for creating a new `DatabaseDefinition` named by the symbol DATASET-NAME."
-  [^clojure.lang.Symbol dataset-name & table-name+field-definition-maps+rows]
+  [^clojure.lang.Symbol dataset-name table-name+field-definition-maps+rows]
   {:pre [(symbol? dataset-name)]}
   `(def ~(vary-meta dataset-name assoc :tag DatabaseDefinition)
-     (create-database-definition ~(name dataset-name)
-       ~@table-name+field-definition-maps+rows)))
-
+     (apply create-database-definition ~(name dataset-name) ~table-name+field-definition-maps+rows)))
 
+(defmacro def-database-definition-edn [dbname]
+  `(def-database-definition ~dbname
+     ~(slurp-edn-table-def (name dbname))))
 
 ;;; ## Convenience + Helper Functions
 ;; TODO - should these go here, or in `metabase.test.data`?
diff --git a/test/metabase/test/data/presto.clj b/test/metabase/test/data/presto.clj
index 62a5ccd742b7f9dfbf9d9448ca6aca050d29fb75..21f4b6550dd5d952f533380ee2b0dc87d3ba8618 100644
--- a/test/metabase/test/data/presto.clj
+++ b/test/metabase/test/data/presto.clj
@@ -41,6 +41,7 @@
       :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"
+      :type/Time       "cast(current_time as TIME)"
       "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/presto-type->base-type field-type))))
@@ -83,7 +84,6 @@
       (doseq [batch batches]
         (#'presto/execute-presto-query! details (insert-sql dbdef tabledef batch))))))
 
-
 ;;; IDriverTestExtensions implementation
 
 (u/strict-extend PrestoDriver
diff --git a/test/metabase/test/data/sqlite.clj b/test/metabase/test/data/sqlite.clj
index 5b2be792012a8348176dd22375f18712576438b2..0c2f3839f44768a67bfe3f38229f5db101874ec9 100644
--- a/test/metabase/test/data/sqlite.clj
+++ b/test/metabase/test/data/sqlite.clj
@@ -27,9 +27,14 @@
   (fn [rows]
     (insert! (for [row rows]
                (into {} (for [[k v] row]
-                          [k (if-not (instance? java.util.Date v)
-                               v
-                               (hsql/call :datetime (hx/literal (u/date->iso-8601 v))))]))))))
+                          [k (cond
+                               (instance? java.sql.Time v)
+                               (hsql/call :time (hx/literal (u/format-time v "UTC")))
+
+                               (instance? java.util.Date v)
+                               (hsql/call :datetime (hx/literal (u/date->iso-8601 v)))
+
+                               :else v)]))))))
 
 (u/strict-extend SQLiteDriver
   generic/IGenericSQLTestExtensions
diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj
index ff7e712e52d25175482cd90e6072da978ccd3b95..b4cb669e60289fbd6a3f434416c4a68e0f6324b5 100644
--- a/test/metabase/test/util.clj
+++ b/test/metabase/test/util.clj
@@ -179,7 +179,9 @@
 
 (u/strict-extend (class PulseCard)
   test/WithTempDefaults
-  {:with-temp-defaults (fn [_] {:position 0})})
+  {:with-temp-defaults (fn [_] {:position    0
+                                :include_csv false
+                                :include_xls false})})
 
 (u/strict-extend (class PulseChannel)
   test/WithTempDefaults
diff --git a/test/metabase/test_setup.clj b/test/metabase/test_setup.clj
index ce79b3578066f9daa68b022a828867a8a2ac617c..b6b56516a7601317008a5251f02759ebf5553546 100644
--- a/test/metabase/test_setup.clj
+++ b/test/metabase/test_setup.clj
@@ -12,7 +12,7 @@
             [metabase.core.initialization-status :as init-status]
             [metabase.models.setting :as setting]))
 
-;; # ---------------------------------------- EXPECTAIONS FRAMEWORK SETTINGS ------------------------------
+;;; ---------------------------------------- Expectations Framework Settings -----------------------------------------
 
 ;; ## GENERAL SETTINGS
 
@@ -61,16 +61,19 @@
                       (< (count e) (count a))             "actual is larger than expected"
                       (> (count e) (count a))             "expected is larger than actual"))))
 
-;; # ------------------------------ FUNCTIONS THAT GET RUN ON TEST SUITE START / STOP ------------------------------
 
-;; `test-startup` function won't work for loading the drivers because they need to be available at evaluation time for some of the unit tests work work properly
+;;; ------------------------------- Functions That Get Ran On Test Suite Start / Stop --------------------------------
+
+;; `test-startup` function won't work for loading the drivers because they need to be available at evaluation time for
+;; some of the unit tests work work properly
 (driver/find-and-load-drivers!)
 
 (defn test-startup
   {:expectations-options :before-run}
   []
   ;; We can shave about a second from unit test launch time by doing the various setup stages in on different threads
-  ;; Start Jetty in the BG so if test setup fails we have an easier time debugging it -- it's trickier to debug things on a BG thread
+  ;; Start Jetty in the BG so if test setup fails we have an easier time debugging it -- it's trickier to debug things
+  ;; on a BG thread
   (let [start-jetty! (future (core/start-jetty!))]
 
     (try
@@ -79,8 +82,8 @@
       (setting/set! :site-name "Metabase Test")
       (init-status/set-complete!)
 
-      ;; make sure the driver test extensions are loaded before running the tests. :reload them because otherwise we get wacky 'method in protocol not implemented' errors
-      ;; when running tests against an individual namespace
+      ;; make sure the driver test extensions are loaded before running the tests. :reload them because otherwise we
+      ;; get wacky 'method in protocol not implemented' errors when running tests against an individual namespace
       (doseq [engine (keys (driver/available-drivers))
               :let   [driver-test-ns (symbol (str "metabase.test.data." (name engine)))]]
         (u/ignore-exceptions
@@ -101,10 +104,8 @@
   (core/stop-jetty!))
 
 (defn call-with-test-scaffolding
-  "Runs `test-startup` and ensures `test-teardown` is always
-  called. This function is useful for running a test (or test
-  namespace) at the repl with the appropriate environment setup for
-  the test to pass."
+  "Runs `test-startup` and ensures `test-teardown` is always called. This function is useful for running a test (or test
+  namespace) at the repl with the appropriate environment setup for the test to pass."
   [f]
   (try
     (test-startup)
diff --git a/test/metabase/util/encryption_test.clj b/test/metabase/util/encryption_test.clj
index cc2471cef79cf689f605a41fca01f6ac995db16a..a858a6b1243f5060b8291ecc9ac9000e685d2155 100644
--- a/test/metabase/util/encryption_test.clj
+++ b/test/metabase/util/encryption_test.clj
@@ -1,4 +1,5 @@
 (ns metabase.util.encryption-test
+  "Tests for encryption of Metabase DB details."
   (:require [clojure.string :as str]
             [expectations :refer :all]
             [metabase.test.util :as tu]
@@ -48,6 +49,7 @@
 
 (expect
   (some (fn [[_ _ message]]
-          (str/includes? message "Cannot decrypt encrypted details. Have you changed or forgot to set MB_ENCRYPTION_SECRET_KEY? Message seems corrupt or manipulated."))
+          (str/includes? message (str "Cannot decrypt encrypted details. Have you changed or forgot to set "
+                                      "MB_ENCRYPTION_SECRET_KEY? Message seems corrupt or manipulated.")))
         (tu/with-log-messages
           (encryption/maybe-decrypt secret-2 (encryption/encrypt secret "WOW")))))
diff --git a/yarn.lock b/yarn.lock
index 9ae2f275d86b8e114e9adf72b9125aa3f7c36c77..d5100b81a481c4927d6c2a5bbc92567e78f4ba39 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5483,8 +5483,8 @@ lcid@^1.0.0:
     invert-kv "^1.0.0"
 
 leaflet-draw@^0.4.9:
-  version "0.4.12"
-  resolved "https://registry.yarnpkg.com/leaflet-draw/-/leaflet-draw-0.4.12.tgz#04c9f3506e3b3a8a488ad389381331dc5b2affd8"
+  version "0.4.13"
+  resolved "https://registry.yarnpkg.com/leaflet-draw/-/leaflet-draw-0.4.13.tgz#b9467d8d6523edc5912e93727951005b77b62895"
 
 leaflet.heat@^0.2.0:
   version "0.2.0"