Skip to content
Snippets Groups Projects
Unverified Commit 9f637335 authored by Cam Saul's avatar Cam Saul Committed by GitHub
Browse files

Merge pull request #9958 from metabase/fix-9942

Fix sql-jdbc QP error responses not being properly reported 
parents aad627e0 230d6871
No related branches found
No related tags found
No related merge requests found
...@@ -180,10 +180,7 @@ ...@@ -180,10 +180,7 @@
;; This is what does the real work of canceling the query. We aren't checking the result of ;; 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. ;; `query-future` but this will cause an exception to be thrown, saying the query has been cancelled.
(.cancel stmt) (.cancel stmt)
(throw e)) (throw e))))))
(catch Exception e
(u/ignore-exceptions (.cancel stmt))
e)))))
(defn- run-query (defn- run-query
"Run the query itself." "Run the query itself."
...@@ -248,8 +245,9 @@ ...@@ -248,8 +245,9 @@
(defn- set-timezone! (defn- set-timezone!
"Set the timezone for the current connection." "Set the timezone for the current connection."
[driver settings connection] {:arglists '([driver settings connection])}
(let [timezone (u/prog1 (:report-timezone settings) [driver {:keys [report-timezone]} connection]
(let [timezone (u/prog1 report-timezone
(assert (re-matches #"[A-Za-z\/_]+" <>))) (assert (re-matches #"[A-Za-z\/_]+" <>)))
format-string (set-timezone-sql driver) format-string (set-timezone-sql driver)
sql (format format-string (str \' timezone \'))] sql (format format-string (str \' timezone \'))]
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
[data :as data] [data :as data]
[util :as tu]] [util :as tu]]
[metabase.test.data [metabase.test.data
[datasets :refer [expect-with-driver]] [datasets :as datasets]
[interface :as tx]] [interface :as tx]]
[toucan.db :as db] [toucan.db :as db]
[toucan.util.test :as tt])) [toucan.util.test :as tt]))
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
:user "camsaul"})) :user "camsaul"}))
;; Verify that we identify JSON columns and mark metadata properly during sync ;; Verify that we identify JSON columns and mark metadata properly during sync
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
:type/SerializedJSON :type/SerializedJSON
(data/with-temp-db (data/with-temp-db
[_ [_
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
[#uuid "84ed434e-80b4-41cf-9c88-e334427104ae"]]]]) [#uuid "84ed434e-80b4-41cf-9c88-e334427104ae"]]]])
;; Check that we can load a Postgres Database with a :type/UUID ;; Check that we can load a Postgres Database with a :type/UUID
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[{:name "id", :base_type :type/Integer} [{:name "id", :base_type :type/Integer}
{:name "user_id", :base_type :type/UUID}] {:name "user_id", :base_type :type/UUID}]
(->> (data/dataset metabase.driver.postgres-test/with-uuid (->> (data/dataset metabase.driver.postgres-test/with-uuid
...@@ -95,21 +95,21 @@ ...@@ -95,21 +95,21 @@
;; Check that we can filter by a UUID Field ;; Check that we can filter by a UUID Field
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[[2 #uuid "4652b2e7-d940-4d55-a971-7e484566663e"]] [[2 #uuid "4652b2e7-d940-4d55-a971-7e484566663e"]]
(rows (data/dataset metabase.driver.postgres-test/with-uuid (rows (data/dataset metabase.driver.postgres-test/with-uuid
(data/run-mbql-query users (data/run-mbql-query users
{:filter [:= $user_id "4652b2e7-d940-4d55-a971-7e484566663e"]})))) {:filter [:= $user_id "4652b2e7-d940-4d55-a971-7e484566663e"]}))))
;; check that a nil value for a UUID field doesn't barf (#2152) ;; check that a nil value for a UUID field doesn't barf (#2152)
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[] []
(rows (data/dataset metabase.driver.postgres-test/with-uuid (rows (data/dataset metabase.driver.postgres-test/with-uuid
(data/run-mbql-query users (data/run-mbql-query users
{:filter [:= $user_id nil]})))) {:filter [:= $user_id nil]}))))
;; Check that we can filter by a UUID for SQL Field filters (#7955) ;; Check that we can filter by a UUID for SQL Field filters (#7955)
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[[#uuid "4f01dcfd-13f7-430c-8e6f-e505c0851027" 1]] [[#uuid "4f01dcfd-13f7-430c-8e6f-e505c0851027" 1]]
(data/dataset metabase.driver.postgres-test/with-uuid (data/dataset metabase.driver.postgres-test/with-uuid
(rows (qp/process-query {:database (data/id) (rows (qp/process-query {:database (data/id)
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
["four_loko"] ["four_loko"]
["ouija_board"]]]]) ["ouija_board"]]]])
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:columns ["id" "dotted.name"] {:columns ["id" "dotted.name"]
:rows [[1 "toucan_cage"] :rows [[1 "toucan_cage"]
[2 "four_loko"] [2 "four_loko"]
...@@ -153,7 +153,7 @@ ...@@ -153,7 +153,7 @@
{:field-name "bird_id", :base-type :type/Integer, :fk :birds}] {:field-name "bird_id", :base-type :type/Integer, :fk :birds}]
[["Cam" 1]]]]) [["Cam" 1]]]])
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:columns ["name" "name_2"] {:columns ["name" "name_2"]
:rows [["Cam" "Rasta"]]} :rows [["Cam" "Rasta"]]}
(-> (data/dataset metabase.driver.postgres-test/duplicate-names (-> (data/dataset metabase.driver.postgres-test/duplicate-names
...@@ -171,7 +171,7 @@ ...@@ -171,7 +171,7 @@
;; Filtering by inet columns should add the appropriate SQL cast, e.g. `cast('192.168.1.1' AS inet)` (otherwise this ;; Filtering by inet columns should add the appropriate SQL cast, e.g. `cast('192.168.1.1' AS inet)` (otherwise this
;; wouldn't work) ;; wouldn't work)
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[[1]] [[1]]
(rows (data/dataset metabase.driver.postgres-test/ip-addresses (rows (data/dataset metabase.driver.postgres-test/ip-addresses
(data/run-mbql-query addresses (data/run-mbql-query addresses
...@@ -200,7 +200,7 @@ ...@@ -200,7 +200,7 @@
;; Check that we properly fetch materialized views. ;; Check that we properly fetch materialized views.
;; As discussed in #2355 they don't come back from JDBC `DatabaseMetadata` so we have to fetch them manually. ;; As discussed in #2355 they don't come back from JDBC `DatabaseMetadata` so we have to fetch them manually.
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:tables #{(default-table-result "test_mview")}} {:tables #{(default-table-result "test_mview")}}
(do (do
(drop-if-exists-and-create-db! "materialized_views_test") (drop-if-exists-and-create-db! "materialized_views_test")
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
(driver/describe-database :postgres database))))) (driver/describe-database :postgres database)))))
;; Check that we properly fetch foreign tables. ;; Check that we properly fetch foreign tables.
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:tables (set (map default-table-result ["foreign_table" "local_table"]))} {:tables (set (map default-table-result ["foreign_table" "local_table"]))}
(do (do
(drop-if-exists-and-create-db! "fdw_test") (drop-if-exists-and-create-db! "fdw_test")
...@@ -232,7 +232,7 @@ ...@@ -232,7 +232,7 @@
;; make sure that if a view is dropped and recreated that the original Table object is marked active rather than a new ;; make sure that if a view is dropped and recreated that the original Table object is marked active rather than a new
;; one being created (#3331) ;; one being created (#3331)
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[{:name "angry_birds", :active true}] [{:name "angry_birds", :active true}]
(let [details (tx/dbdef->connection-details :postgres :db {:database-name "dropped_views_test"}) (let [details (tx/dbdef->connection-details :postgres :db {:database-name "dropped_views_test"})
spec (sql-jdbc.conn/connection-details->spec :postgres details) spec (sql-jdbc.conn/connection-details->spec :postgres details)
...@@ -270,18 +270,18 @@ ...@@ -270,18 +270,18 @@
{:query "SELECT current_setting('TIMEZONE') AS timezone;"})))) {:query "SELECT current_setting('TIMEZONE') AS timezone;"}))))
;; check that if we set report-timezone to US/Pacific that the session timezone is in fact US/Pacific ;; check that if we set report-timezone to US/Pacific that the session timezone is in fact US/Pacific
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
"US/Pacific" "US/Pacific"
(get-timezone-with-report-timezone "US/Pacific")) (get-timezone-with-report-timezone "US/Pacific"))
;; check that we can set it to something else: America/Chicago ;; check that we can set it to something else: America/Chicago
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
"America/Chicago" "America/Chicago"
(get-timezone-with-report-timezone "America/Chicago")) (get-timezone-with-report-timezone "America/Chicago"))
;; ok, check that if we try to put in a fake timezone that the query still reëxecutes without a custom timezone. This ;; ok, check that if we try to put in a fake timezone that the query still reëxecutes without a custom timezone. This
;; should give us the same result as if we didn't try to set a timezone at all ;; should give us the same result as if we didn't try to set a timezone at all
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
(get-timezone-with-report-timezone nil) (get-timezone-with-report-timezone nil)
(get-timezone-with-report-timezone "Crunk Burger")) (get-timezone-with-report-timezone "Crunk Burger"))
...@@ -299,12 +299,12 @@ ...@@ -299,12 +299,12 @@
:dbname "cool" :dbname "cool"
:additional-options "prepareThreshold=0"})) :additional-options "prepareThreshold=0"}))
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
"UTC" "UTC"
(tu/db-timezone-id)) (tu/db-timezone-id))
;; Make sure we're able to fingerprint TIME fields (#5911) ;; Make sure we're able to fingerprint TIME fields (#5911)
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
#{#metabase.models.field.FieldInstance{:name "start_time", :fingerprint {:global {:distinct-count 1 #{#metabase.models.field.FieldInstance{:name "start_time", :fingerprint {:global {:distinct-count 1
:nil% 0.0} :nil% 0.0}
:type {:type/DateTime {:earliest "1970-01-01T22:00:00.000Z", :latest "1970-01-01T22:00:00.000Z"}}}} :type {:type/DateTime {:earliest "1970-01-01T22:00:00.000Z", :latest "1970-01-01T22:00:00.000Z"}}}}
...@@ -366,14 +366,14 @@ ...@@ -366,14 +366,14 @@
(f database))) (f database)))
;; check that we can actually fetch the enum types from a DB ;; check that we can actually fetch the enum types from a DB
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
#{(keyword "bird type") :bird_status} #{(keyword "bird type") :bird_status}
(do-with-enums-db (do-with-enums-db
(fn [db] (fn [db]
(#'postgres/enum-types :postgres db)))) (#'postgres/enum-types :postgres db))))
;; check that describe-table properly describes the database & base types of the enum fields ;; check that describe-table properly describes the database & base types of the enum fields
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:name "birds" {:name "birds"
:fields #{{:name "name", :fields #{{:name "name",
:database-type "varchar" :database-type "varchar"
...@@ -390,7 +390,7 @@ ...@@ -390,7 +390,7 @@
(driver/describe-table :postgres db {:name "birds"})))) (driver/describe-table :postgres db {:name "birds"}))))
;; check that when syncing the DB the enum types get recorded appropriately ;; check that when syncing the DB the enum types get recorded appropriately
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
#{{:name "name", :database_type "varchar", :base_type :type/Text} #{{:name "name", :database_type "varchar", :base_type :type/Text}
{:name "type", :database_type "bird type", :base_type :type/PostgresEnum} {:name "type", :database_type "bird type", :base_type :type/PostgresEnum}
{:name "status", :database_type "bird_status", :base_type :type/PostgresEnum}} {:name "status", :database_type "bird_status", :base_type :type/PostgresEnum}}
...@@ -402,12 +402,12 @@ ...@@ -402,12 +402,12 @@
;; check that values for enum types get wrapped in appropriate CAST() fn calls in `->honeysql` ;; check that values for enum types get wrapped in appropriate CAST() fn calls in `->honeysql`
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:name :cast, :args ["toucan" (keyword "bird type")]} {:name :cast, :args ["toucan" (keyword "bird type")]}
(sql.qp/->honeysql :postgres [:value "toucan" {:database_type "bird type", :base_type :type/PostgresEnum}])) (sql.qp/->honeysql :postgres [:value "toucan" {:database_type "bird type", :base_type :type/PostgresEnum}]))
;; End-to-end check: make sure everything works as expected when we run an actual query ;; End-to-end check: make sure everything works as expected when we run an actual query
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
{:rows [["Rasta" "good bird" "toucan"]] {:rows [["Rasta" "good bird" "toucan"]]
:native_form {:query (str "SELECT \"public\".\"birds\".\"name\" AS \"name\"," :native_form {:query (str "SELECT \"public\".\"birds\".\"name\" AS \"name\","
" \"public\".\"birds\".\"status\" AS \"status\"," " \"public\".\"birds\".\"status\" AS \"status\","
...@@ -430,7 +430,7 @@ ...@@ -430,7 +430,7 @@
(select-keys [:rows :native_form])))))) (select-keys [:rows :native_form]))))))
;; make sure schema/table/field names with hyphens in them work correctly (#8766) ;; make sure schema/table/field names with hyphens in them work correctly (#8766)
(expect-with-driver :postgres (datasets/expect-with-driver :postgres
[["Bird Hat"]] [["Bird Hat"]]
(metabase.driver/with-driver :postgres (metabase.driver/with-driver :postgres
[{:name "angry_birds", :active true}] [{:name "angry_birds", :active true}]
...@@ -453,3 +453,14 @@ ...@@ -453,3 +453,14 @@
:type :query :type :query
:query {:source-table (db/select-one-id Table :name "presents-and-gifts")}}) :query {:source-table (db/select-one-id Table :name "presents-and-gifts")}})
rows)))))) rows))))))
;; If the DB throws an exception, is it properly returned by the query processor? Is it status :failed? (#9942)
(datasets/expect-with-driver :postgres
{:status :failed
:class org.postgresql.util.PSQLException
:error "ERROR: column \"adsasdasd\" does not exist\n Position: 20"}
(-> (qp/process-query
{:database (data/id)
:type :native
:native {:query "SELECT adsasdasd;"}})
(select-keys [:status :class :error])))
...@@ -7,11 +7,16 @@ ...@@ -7,11 +7,16 @@
[query-processor :as qp] [query-processor :as qp]
[util :as u]] [util :as u]]
[metabase.driver.sql-jdbc-test :as sql-jdbc-test] [metabase.driver.sql-jdbc-test :as sql-jdbc-test]
[metabase.driver.sql-jdbc.execute :as sql-jdbc.execute]
[metabase.query-processor
[store :as qp.store]
[test-util :as qp.tu]]
[metabase.test [metabase.test
[data :as data] [data :as data]
[util :as tu]] [util :as tu]]
[metabase.test.data.datasets :as datasets] [metabase.test.data.datasets :as datasets]
[metabase.test.util.log :as tu.log]) [metabase.test.util.log :as tu.log]
[schema.core :as s])
(:import [java.sql PreparedStatement ResultSet])) (:import [java.sql PreparedStatement ResultSet]))
(defn- do-with-max-rows [f] (defn- do-with-max-rows [f]
...@@ -185,3 +190,60 @@ ...@@ -185,3 +190,60 @@
(query-thunk)) (query-thunk))
(catch Throwable e (catch Throwable e
(throw e))))))))) (throw e)))))))))
;; If the DB throws an exception, is it properly returned by the query processor? Is it status :failed? (#9942)
(tu/expect-schema
{:status (s/eq :failed)
:class (s/eq java.lang.Exception)
:error (s/eq "Column \"ADSASDASD\" not found")
:stacktrace [s/Str]
:query s/Any}
(qp/process-query
{:database (data/id)
:type :native
:native {:query "SELECT adsasdasd;"}}))
;; do we run query with a timezone if one is present in the Settings?
(defn- ran-with-timezone? [driver query]
(let [ran-with-timezone? (promise)
timezone (promise)]
(with-redefs [sql-jdbc.execute/run-query-without-timezone
(let [orig @#'sql-jdbc.execute/run-query-without-timezone]
(fn [& args]
(deliver ran-with-timezone? false)
(deliver timezone nil)
(apply orig args)))
sql-jdbc.execute/run-query-with-timezone
(let [orig @#'sql-jdbc.execute/run-query-with-timezone]
(fn [& args]
(deliver ran-with-timezone? true)
(apply orig args)))
sql-jdbc.execute/set-timezone!
(let [orig @#'sql-jdbc.execute/set-timezone!]
(fn [driver {:keys [report-timezone], :as settings} connection]
(deliver timezone report-timezone)
(orig driver settings connection)))]
(qp.tu/with-everything-store
(qp.store/store-database! (data/db))
(sql-jdbc.execute/execute-query driver query))
{:ran-with-timezone? (u/deref-with-timeout ran-with-timezone? 1000)
:timezone (u/deref-with-timeout timezone 1000)})))
(expect
{:ran-with-timezone? false, :timezone nil}
(ran-with-timezone?
:h2
{:database (data/id)
:type :native
:native {:query "SELECT * FROM VENUES LIMIT 1;"}}))
(expect
{:ran-with-timezone? true, :timezone "US/Pacific"}
(ran-with-timezone?
:h2
{:database (data/id)
:type :native
:native {:query "SELECT * FROM VENUES LIMIT 1;"}
:settings {:report-timezone "US/Pacific"}}))
...@@ -12,16 +12,19 @@ ...@@ -12,16 +12,19 @@
[f] [f]
(with-redefs [qp.store/table (fn [table-id] (with-redefs [qp.store/table (fn [table-id]
(or (get-in @@#'qp.store/*store* [:tables table-id]) (or (get-in @@#'qp.store/*store* [:tables table-id])
(db/select-one (vec (cons Table qp.store/table-columns-to-fetch)), :id table-id))) (db/select-one (into [Table] qp.store/table-columns-to-fetch), :id table-id)))
qp.store/field (fn [field-id] qp.store/field (fn [field-id]
(or (get-in @@#'qp.store/*store* [:fields field-id]) (or (get-in @@#'qp.store/*store* [:fields field-id])
(db/select-one (vec (cons Field qp.store/field-columns-to-fetch)), :id field-id)))] (db/select-one (into [Field] qp.store/field-columns-to-fetch), :id field-id)))]
(qp.store/with-store (qp.store/with-store
(f)))) (f))))
(defmacro with-everything-store (defmacro with-everything-store
"When testing a specific piece of middleware, you often need to load things into the QP store, but doing so can be "When testing a specific piece of middleware, you often need to load things into the QP store, but doing so can be
tedious. This macro swaps out the normal QP store backend with one that fetches Tables and Fields from the DB tedious. This macro swaps out the normal QP store backend with one that fetches Tables and Fields from the DB
on-demand, making tests a lot nicer to write." on-demand, making tests a lot nicer to write.
You still need to store the Database yourself, because the getter function takes no args; we don't know which DB
you'd want to fetch."
[& body] [& body]
`(do-with-everything-store (fn [] ~@body))) `(do-with-everything-store (fn [] ~@body)))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment