diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index 941e90df4c9e1aa19d5fcd9db329626e5075ab9f..47066c03f848bccc21d7ac5bac6eb73f86ce7e68 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -425,7 +425,7 @@ export default class Visualization extends Component {
     }
 
     return (
-      <div className={cx(className, "flex flex-column")}>
+      <div className={cx(className, "flex flex-column full-height")}>
         {(showTitle &&
           (settings["card.title"] || extra) &&
           (loading ||
diff --git a/frontend/src/metabase/visualizations/visualizations/Text.css b/frontend/src/metabase/visualizations/visualizations/Text.css
index 49290aac48694d6b05bc225ca4b0c4bbf5d4bbad..a51ead787990b693839712dd7025e78e28d1dda8 100644
--- a/frontend/src/metabase/visualizations/visualizations/Text.css
+++ b/frontend/src/metabase/visualizations/visualizations/Text.css
@@ -7,6 +7,7 @@
   flex-direction: column;
   justify-content: center;
   padding: 1em 0 1em 1.5em;
+  height: 100%;
 }
 :local .Text.dashboard-is-editing {
   padding: 2.8em 1.3em var(--text-card-padding) 1.3em;
@@ -26,6 +27,7 @@
   box-shadow: 0 1px 7px var(--color-shadow);
 }
 :local .Text .text-card-markdown {
+  height: 100%;
   overflow: auto;
   pointer-events: all;
 }
diff --git a/modules/drivers/googleanalytics/src/metabase/driver/googleanalytics/query_processor.clj b/modules/drivers/googleanalytics/src/metabase/driver/googleanalytics/query_processor.clj
index 2249665ea03eb63bd73e7b033ad09678f2d60c85..4026997ea086551f6460a3129013b1986012f9d1 100644
--- a/modules/drivers/googleanalytics/src/metabase/driver/googleanalytics/query_processor.clj
+++ b/modules/drivers/googleanalytics/src/metabase/driver/googleanalytics/query_processor.clj
@@ -177,24 +177,27 @@
 
 ;;; ----------------------------------------------- filter (intervals) -----------------------------------------------
 
-
-(defn- date-sub-day [[clause time-component unit :as value]]
-  (case clause
-    :absolute-datetime [:absolute-datetime (du/relative-date :day -1 (du/date-trunc unit time-component)) :day]
-    :relative-datetime (if (= unit :day)
-                         [clause (- time-component 1) unit]
-                         [:absolute-datetime
-                          (du/relative-date :day -1 (du/date-trunc unit (du/relative-date unit time-component))) :day])
-    value))
-
-(defn- date-add-day [[clause time-component unit :as value]]
-  (case clause
-    :absolute-datetime [:absolute-datetime (du/relative-date :day 1 (du/date-trunc unit time-component)) unit]
-    :relative-datetime (if (= unit :day)
-                         [clause (+ time-component 1) unit]
-                         [:absolute-datetime
-                          (du/relative-date :day 1 (du/date-trunc unit (du/relative-date unit time-component))) :day])
-    value))
+(defn- date-add-days
+  "Add `n-days` to a datetime clause (`:absolute-datetime` or `:relative-datetime`) Done to fix off-by-one issues with
+  GA. See #9904"
+  [[clause-name time-component unit, :as datetime-clause] n-days]
+  (case clause-name
+    :absolute-datetime
+    [:absolute-datetime (du/relative-date :day n-days (du/date-trunc unit time-component)) unit]
+
+    :relative-datetime
+    (if (= unit :day)
+      [clause-name (+ time-component n-days) unit]
+      [:absolute-datetime
+       (du/relative-date :day n-days (du/date-trunc unit (du/relative-date unit time-component))) :day])
+
+    datetime-clause))
+
+(defn- date-sub-day [datetime-clause]
+  (date-add-days datetime-clause -1))
+
+(defn- date-add-day [datetime-clause]
+  (date-add-days datetime-clause 1))
 
 
 (defmulti ^:private parse-filter:interval mbql.u/dispatch-by-clause-name-or-class)
diff --git a/modules/drivers/redshift/project.clj b/modules/drivers/redshift/project.clj
index 9f3aa4787e53d3a15e5ddd7add4f99818d95083e..1b23b2c045a578b0a859eb237d2a001431b848b0 100644
--- a/modules/drivers/redshift/project.clj
+++ b/modules/drivers/redshift/project.clj
@@ -6,7 +6,7 @@
 
 
   :dependencies
-  [[com.amazon.redshift/redshift-jdbc42-no-awssdk "1.2.18.1036"]]
+  [[com.amazon.redshift/redshift-jdbc42-no-awssdk "1.2.27.1051"]]
 
   :profiles
   {:provided
diff --git a/resources/log4j.properties b/resources/log4j.properties
index 3bd60709b70df5f7a51f5a2c5815d95413ef675a..f83d95bdea032d463cde657f0b31775a6af0c4f2 100644
--- a/resources/log4j.properties
+++ b/resources/log4j.properties
@@ -1,4 +1,4 @@
-log4j.rootLogger=WARN, console, metabase
+log4j.rootLogger=WARN, console
 
 # log to the console
 log4j.appender.console=org.apache.log4j.ConsoleAppender
diff --git a/src/metabase/driver/sql_jdbc/execute.clj b/src/metabase/driver/sql_jdbc/execute.clj
index 105f52f4516323a16617698f8a1140da83832f46..ea49bb7e11d19f7513df6c0db0c4d806a721f2a1 100644
--- a/src/metabase/driver/sql_jdbc/execute.clj
+++ b/src/metabase/driver/sql_jdbc/execute.clj
@@ -1,4 +1,3 @@
-
 (ns metabase.driver.sql-jdbc.execute
   "Code related to actually running a SQL query against a JDBC database (including setting the session timezone when
   appropriate), and for properly encoding/decoding types going in and out of the database."
@@ -164,7 +163,8 @@
   `(do-with-ensured-connection ~db (fn [~conn-binding] ~@body)))
 
 (defn- cancelable-run-query
-  "Runs `sql` in such a way that it can be interrupted via a `future-cancel`"
+  "Runs JDBC query, canceling it if an InterruptedException is caught (e.g. if there query is canceled before
+  finishing)."
   [db sql params opts]
   (with-ensured-connection [conn db]
     ;; This is normally done for us by java.jdbc as a result of our `jdbc/query` call
@@ -173,19 +173,17 @@
       ;; (Not all drivers support this so ignore Exceptions if they don't)
       (u/ignore-exceptions
         (.closeOnCompletion stmt))
-      ;; Need to run the query in another thread so that this thread can cancel it if need be
       (try
-        (let [query-future (future (jdbc/query conn (into [stmt] params) opts))]
-          ;; This thread is interruptable because it's awaiting the other thread (the one actually running the
-          ;; query). Interrupting this thread means that the client has disconnected (or we're shutting down) and so
-          ;; we can give up on the query running in the future
-          @query-future)
+        (jdbc/query conn (into [stmt] params) opts)
         (catch InterruptedException e
           (log/warn (tru "Client closed connection, canceling query"))
           ;; This is what does the real work of canceling the query. We aren't checking the result of
           ;; `query-future` but this will cause an exception to be thrown, saying the query has been cancelled.
           (.cancel stmt)
-          (throw e))))))
+          (throw e))
+        (catch Exception e
+          (u/ignore-exceptions (.cancel stmt))
+          e)))))
 
 (defn- run-query
   "Run the query itself."
@@ -205,10 +203,12 @@
 ;;; -------------------------- Running queries: exception handling & disabling auto-commit ---------------------------
 
 (defn- exception->nice-error-message ^String [^SQLException e]
-  (or (->> (.getMessage e)     ; error message comes back like 'Column "ZID" not found; SQL statement: ... [error-code]' sometimes
-           (re-find #"^(.*);") ; the user already knows the SQL, and error code is meaningless
-           second)             ; so just return the part of the exception that is relevant
-      (.getMessage e)))
+  ;; error message comes back like 'Column "ZID" not found; SQL statement: ... [error-code]' sometimes
+  ;; the user already knows the SQL, and error code is meaningless
+  ;; so just return the part of the exception that is relevant
+  (->> (.getMessage e)
+       (re-find #"^(.*);")
+       second))
 
 (defn do-with-try-catch
   "Tries to run the function `f`, catching and printing exception chains if SQLException is thrown,
@@ -219,7 +219,10 @@
     (f)
     (catch SQLException e
       (log/error (jdbc/print-sql-exception-chain e))
-      (throw (Exception. (exception->nice-error-message e) e)))))
+      (throw
+       (if-let [nice-error-message (exception->nice-error-message e)]
+         (Exception. nice-error-message e)
+         e)))))
 
 (defn- do-with-auto-commit-disabled
   "Disable auto-commit for this transaction, and make the transaction `rollback-only`, which means when the
diff --git a/src/metabase/sync/sync_metadata/fields/sync_metadata.clj b/src/metabase/sync/sync_metadata/fields/sync_metadata.clj
index 903e1ec3c691ef4bc6c580393c3e39a2eb9dca7c..a5a4409354215a6307018ae3384e4cb3fda5781f 100644
--- a/src/metabase/sync/sync_metadata/fields/sync_metadata.clj
+++ b/src/metabase/sync/sync_metadata/fields/sync_metadata.clj
@@ -27,6 +27,7 @@
         {new-database-type :database-type
          new-base-type     :base-type
          new-field-comment :field-comment} field-metadata
+        new-database-type                  (or new-database-type "NULL")
         new-special-type                   (common/special-type field-metadata)
 
         new-db-type?
diff --git a/test/metabase/driver/sql_jdbc/execute_test.clj b/test/metabase/driver/sql_jdbc/execute_test.clj
index 4fc8723dd28875c0abdd5e2518dd5cc1d05de69d..6c201b7900e8fbefd67557844d01e7ad8399ef46 100644
--- a/test/metabase/driver/sql_jdbc/execute_test.clj
+++ b/test/metabase/driver/sql_jdbc/execute_test.clj
@@ -1,10 +1,18 @@
 (ns metabase.driver.sql-jdbc.execute-test
-  (:require [clojure.java.jdbc :as jdbc]
+  (:require [clojure.core.async :as a]
+            [clojure.java.jdbc :as jdbc]
+            [clojure.string :as str]
+            [expectations :refer [expect]]
+            [metabase
+             [query-processor :as qp]
+             [util :as u]]
             [metabase.driver.sql-jdbc-test :as sql-jdbc-test]
-            [metabase.query-processor :as qp]
-            [metabase.test.data :as data]
-            [metabase.test.data.datasets :as datasets])
-  (:import java.sql.PreparedStatement))
+            [metabase.test
+             [data :as data]
+             [util :as tu]]
+            [metabase.test.data.datasets :as datasets]
+            [metabase.test.util.log :as tu.log])
+  (:import [java.sql PreparedStatement ResultSet]))
 
 (defn- do-with-max-rows [f]
   ;; force loading of the test data before swapping out `jdbc/query`, otherwise things might not sync correctly
@@ -59,3 +67,121 @@
                                        :type     :query
                                        :query    {:source-table (data/id :venues)}})
        :constraints {:max-results 15}})))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                   Query Cancelation & Resource Closing Tests                                   |
+;;; +----------------------------------------------------------------------------------------------------------------+
+
+(defrecord ^:private FakePreparedStatement [called-cancel? called-close? fake-resultset]
+  java.sql.PreparedStatement
+  (closeOnCompletion [_])               ; no-op
+  (cancel [_] (some-> called-cancel? (deliver true)))
+  (close [_] (some-> called-close? (deliver true)))
+  (executeQuery [_] fake-resultset))
+
+(defrecord ^:private FakeResultSet [called-close?]
+  ResultSet
+  (close [_] (some-> called-close? (deliver true)))
+  (next [_] false)
+  (getMetaData [_]
+    (reify java.sql.ResultSetMetaData
+      (getColumnCount [_] 0))))
+
+(defn- fake-prepare-statement
+  "Returns `fake-value` whenenver the `sql` parameter returns a truthy value when passed to `use-fake-value?`."
+  [& {:keys [use-fake-value? faked-value]
+      :or   {use-fake-value? (constantly false)}}]
+  (let [original-prepare-statement jdbc/prepare-statement]
+    (fn [connection sql options]
+      (if (use-fake-value? sql)
+        faked-value
+        (original-prepare-statement connection sql options)))))
+
+(defn- fake-query
+  "Function to replace the `clojure.java.jdbc/query` function. Will invoke `on-fake-prepared-statement` when passed an
+  instance of `FakePreparedStatement`."
+  [& {:keys [on-fake-prepared-statement]}]
+  (let [original-query jdbc/query]
+    (fn
+      ([conn stmt+params]
+       (conn stmt+params))
+
+      ([conn stmt+params opts]
+       (if (instance? FakePreparedStatement (first stmt+params))
+         (when on-fake-prepared-statement (on-fake-prepared-statement))
+         (original-query conn stmt+params opts))))))
+
+;; make sure queries properly clean up after themselves and close result sets and prepared statements when finished
+(expect
+  {:prepared-statement-closed? true, :result-set-closed? true}
+  (let [closed-prepared-statement? (promise)
+        closed-result-set?         (promise)
+        fake-resultset             (->FakeResultSet closed-result-set?)
+        fake-prepared-statement    (->FakePreparedStatement nil closed-prepared-statement? fake-resultset)
+        placeholder-query          "SELECT 1;"]
+    (with-redefs [jdbc/prepare-statement (fake-prepare-statement
+                                          :use-fake-value? #(str/includes? % placeholder-query)
+                                          :faked-value fake-prepared-statement)]
+      (let [{{:keys [rows]} :data, :as results} (qp/process-query
+                                                  {:database (data/id)
+                                                   :type     :native
+                                                   :native   {:query placeholder-query}})]
+        (when-not rows
+          (throw (ex-info "Query failed to run!" results)))
+        ;; make sure results are fully realized!
+        (mapv vec rows))
+      {:prepared-statement-closed? (deref closed-prepared-statement? 1000 false)
+       :result-set-closed?         (deref closed-result-set?         1000 false)})))
+
+;; make sure a prepared statement is canceled & closed when the async QP channel is closed
+(expect
+  {:prepared-statement-canceled? true, :prepared-statement-closed? true}
+  (let [started?                     (promise)
+        canceled-prepared-statement? (promise)
+        closed-prepared-statement?   (promise)
+        fake-prepared-statement      (->FakePreparedStatement canceled-prepared-statement? closed-prepared-statement? nil)
+        placeholder-query            "SLEEP PLACEHOLDER"]
+    (with-redefs [jdbc/prepare-statement (fake-prepare-statement
+                                          :use-fake-value? #(str/includes? % placeholder-query)
+                                          :faked-value fake-prepared-statement)
+                  jdbc/query             (fake-query
+                                          :on-fake-prepared-statement
+                                          (fn [& _]
+                                            (deliver started? true)
+                                            (Thread/sleep 2000)
+                                            (->FakeResultSet nil)))]
+      (let [chan (qp/process-query
+                   {:database (data/id)
+                    :type     :native
+                    :native   {:query placeholder-query}
+                    :async?   true})]
+        (u/deref-with-timeout started? 1000)
+        (a/close! chan)
+        {:prepared-statement-canceled? (deref canceled-prepared-statement? 3000 false)
+         :prepared-statement-closed?   (deref closed-prepared-statement?   1000 false)}))))
+
+;; The test below sort of tests the same thing (I think). But it is written in a much more confusing manner. If you
+;; dare, try to work out why happens by reading source `call-with-paused-query`. - Cam
+(expect
+  ::tu/success
+  ;; this might dump messages about the connection being closed; we don't need to worry about that
+  (tu.log/suppress-output
+    (tu/call-with-paused-query
+     (fn [query-thunk called-query? called-cancel? pause-query]
+       (let [ ;; This fake prepared statement is cancelable like a prepared statement, but will allow us to tell the
+             ;; difference between our Prepared statement and the real thing
+             fake-prep-stmt  (->FakePreparedStatement called-cancel? nil nil)]
+         (future
+           (try
+             (with-redefs [jdbc/prepare-statement (fake-prepare-statement
+                                                   :use-fake-value? (fn [sql] (re-find #"checkins" (str/lower-case sql)))
+                                                   :faked-value     fake-prep-stmt)
+                           jdbc/query             (fake-query
+                                                   :on-fake-prepared-statement
+                                                   (fn []
+                                                     (deliver called-query? true)
+                                                     @pause-query))]
+               (query-thunk))
+             (catch Throwable e
+               (throw e)))))))))
diff --git a/test/metabase/query_processor_test/failure_test.clj b/test/metabase/query_processor_test/failure_test.clj
index 5dfde5b29ee4b618aa4a2cdcbb6c02fe56a9b327..8576b6d2f0960fe84f957da7f1541bfef4eedcb6 100644
--- a/test/metabase/query_processor_test/failure_test.clj
+++ b/test/metabase/query_processor_test/failure_test.clj
@@ -4,7 +4,9 @@
             [medley.core :as m]
             [metabase.query-processor :as qp]
             [metabase.query-processor.interface :as qp.i]
-            [metabase.test.data :as data]))
+            [metabase.test.data :as data]
+            [metabase.util.schema :as su]
+            [schema.core :as s]))
 
 (defn- bad-query []
   {:database (data/id)
@@ -27,12 +29,13 @@
                 "LIMIT 1048576")
    :params nil})
 
-(def ^:private ^{:arglists '([stacktrace])} valid-stacktrace? (every-pred seq (partial every? (every-pred string? seq))))
+(def ^:private ^{:arglists '([stacktrace])} valid-stacktrace?
+  (complement (partial s/check [su/NonBlankString])))
 
 ;; running a bad query via `process-query` should return stacktrace, query, preprocessed query, and native query
 (expect
   {:status       :failed
-   :class        java.util.concurrent.ExecutionException
+   :class        Exception
    :error        true
    :stacktrace   true
    ;; `:database` is removed by the catch-exceptions middleware for historical reasons
diff --git a/test/metabase/query_processor_test/query_cancellation_test.clj b/test/metabase/query_processor_test/query_cancellation_test.clj
deleted file mode 100644
index 65227793bfe38a3511171e0b969be8d7bc46c843..0000000000000000000000000000000000000000
--- a/test/metabase/query_processor_test/query_cancellation_test.clj
+++ /dev/null
@@ -1,65 +0,0 @@
-(ns metabase.query-processor-test.query-cancellation-test
-  "TODO - This is sql-jdbc specific, so it should go in a sql-jdbc test namespace."
-  (:require [clojure.java.jdbc :as jdbc]
-            [clojure.string :as str]
-            [expectations :refer [expect]]
-            [metabase.test.util :as tu]
-            [metabase.test.util.log :as tu.log]))
-
-(defrecord ^:private FakePreparedStatement [called-cancel?]
-  java.sql.PreparedStatement
-  (closeOnCompletion [_]) ; no-op
-  (cancel [_] (deliver called-cancel? true))
-  (close [_] true))
-
-(defn- make-fake-prep-stmt
-  "Returns `fake-value` whenenver the `sql` parameter returns a truthy value when passed to `use-fake-value?`."
-  [orig-make-prep-stmt & {:keys [use-fake-value? faked-value]
-                          :or   {use-fake-value? (constantly false)}}]
-  (fn [connection sql options]
-    (if (use-fake-value? sql)
-      faked-value
-      (orig-make-prep-stmt connection sql options))))
-
-(defn- fake-query
-  "Function to replace the `clojure.java.jdbc/query` function. Will invoke `on-fake-prepared-statement` when passed an
-  instance of `FakePreparedStatement`."
-  {:style/indent 1}
-  [orig-jdbc-query & {:keys [on-fake-prepared-statement]}]
-  (fn
-    ([conn stmt+params]
-     (orig-jdbc-query conn stmt+params))
-
-    ([conn stmt+params opts]
-     (if (instance? FakePreparedStatement (first stmt+params))
-       (when on-fake-prepared-statement (on-fake-prepared-statement))
-       (orig-jdbc-query conn stmt+params opts)))))
-
-(expect
-  ::tu/success
-  ;; this might dump messages about the connection being closed; we don't need to worry about that
-  (tu.log/suppress-output
-    (tu/call-with-paused-query
-     (fn [query-thunk called-query? called-cancel? pause-query]
-       (let [ ;; This fake prepared statement is cancelable like a prepared statement, but will allow us to tell the
-             ;; difference between our Prepared statement and the real thing
-             fake-prep-stmt  (->FakePreparedStatement called-cancel?)
-             ;; Much of the underlying plumbing of MB requires a working jdbc/query and jdbc/prepared-statement (such
-             ;; as queryies for the application database). Let binding the original versions of the functions allows
-             ;; us to delegate to them when it's not the query we're trying to test
-             orig-jdbc-query jdbc/query
-             orig-prep-stmt  jdbc/prepare-statement]
-         (future
-           (try
-             (with-redefs [jdbc/prepare-statement (make-fake-prep-stmt
-                                                   orig-prep-stmt
-                                                   :use-fake-value? (fn [sql] (re-find #"checkins" (str/lower-case sql)))
-                                                   :faked-value     fake-prep-stmt)
-                           jdbc/query             (fake-query orig-jdbc-query
-                                                    :on-fake-prepared-statement
-                                                    (fn []
-                                                      (deliver called-query? true)
-                                                      @pause-query))]
-               (query-thunk))
-             (catch Throwable e
-               (throw e)))))))))
diff --git a/test/metabase/sync/sync_metadata/fields/sync_metadata_test.clj b/test/metabase/sync/sync_metadata/fields/sync_metadata_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..01ff41e72329bd18804a9611ae8d43e02689632c
--- /dev/null
+++ b/test/metabase/sync/sync_metadata/fields/sync_metadata_test.clj
@@ -0,0 +1,66 @@
+(ns metabase.sync.sync-metadata.fields.sync-metadata-test
+  (:require [expectations :refer [expect]]
+            [metabase.models.table :refer [Table]]
+            [metabase.sync.sync-metadata.fields.sync-metadata :as sync-metadata]
+            [toucan.db :as db]
+            [toucan.util.test :as tt]))
+
+(defn- updates-that-will-be-performed [new-metadata-from-sync metadata-in-application-db]
+  (tt/with-temp Table [table]
+    (let [update-operations (atom [])]
+      (with-redefs [db/update! (fn [model id updates]
+                                 (swap! update-operations conj [(name model) id updates]))]
+        (#'sync-metadata/update-field-metadata-if-needed!
+         table
+         new-metadata-from-sync
+         metadata-in-application-db)
+        @update-operations))))
+
+;; test that if database-type changes we will update it in the DB
+(expect
+  [["Field" 1 {:database_type "Integer"}]]
+  (updates-that-will-be-performed
+   {:name          "My Field"
+    :database-type "Integer"
+    :base-type     :type/Integer}
+   {:name          "My Field"
+    :database-type "NULL"
+    :base-type     :type/Integer
+    :id            1}))
+
+;; no changes should be made (i.e., no calls to `update!`) if nothing changes
+(expect
+ []
+ (updates-that-will-be-performed
+  {:name          "My Field"
+   :database-type "Integer"
+   :base-type     :type/Integer}
+  {:name          "My Field"
+   :database-type "Integer"
+   :base-type     :type/Integer
+   :id            1}))
+
+;; test that if `database-type` comes back as `nil` in the metadata from the sync process, we won't try to set a `nil`
+;; value in the DB -- this is against the rules -- we should set `NULL` instead. See `TableMetadataField` schema.
+(expect
+  [["Field" 1 {:database_type "NULL"}]]
+  (updates-that-will-be-performed
+   {:name          "My Field"
+    :database-type nil
+    :base-type     :type/Integer}
+   {:name          "My Field"
+    :database-type "Integer"
+    :base-type     :type/Integer
+    :id            1}))
+
+;; if `database-type` comes back as `nil` and was already saved in application DB as `NULL` no changes should be made
+(expect
+  []
+  (updates-that-will-be-performed
+   {:name          "My Field"
+    :database-type nil
+    :base-type     :type/Integer}
+   {:name          "My Field"
+    :database-type "NULL"
+    :base-type     :type/Integer
+    :id            1}))