diff --git a/src/metabase/db/connection_pool.clj b/src/metabase/db/connection_pool.clj index 98ee41951f077b9c06ca09c3a845c1662834f393..75d55f26f47ce669c060c1eaaad11a9090ee30bf 100644 --- a/src/metabase/db/connection_pool.clj +++ b/src/metabase/db/connection_pool.clj @@ -5,7 +5,9 @@ The aim here is to completely encapsulate the connection pool library we use -- that way we can swap it out if we want to at some point without having to touch any other files. (TODO - this is currently true of everything except for the options, which are c3p0-specific -- consider abstracting those as well?)" - (:require [metabase.util :as u]) + (:require [metabase.util :as u] + [metabase.util.schema :as su] + [schema.core :as s]) (:import com.mchange.v2.c3p0.DataSources [java.sql Driver DriverManager] [java.util Map Properties] @@ -46,13 +48,19 @@ (defn- spec->properties ^Properties [spec] (map->properties (dissoc spec :classname :subprotocol :subname))) -(defn- unpooled-data-source ^DataSource [{:keys [subname subprotocol], :as spec}] - {:pre [(string? subname) (string? subprotocol)]} +(def ^:private JDBCSpec + {:subname su/NonBlankString + :subprotocol su/NonBlankString + s/Any s/Any}) + +(s/defn ^:private unpooled-data-source :- DataSource + [{:keys [subname subprotocol], :as spec} :- JDBCSpec] (proxy-data-source (format "jdbc:%s:%s" subprotocol subname) (spec->properties spec))) (defn- pooled-data-source ^DataSource ([spec] (DataSources/pooledDataSource (unpooled-data-source spec))) + ([spec, ^Map pool-properties] (DataSources/pooledDataSource (unpooled-data-source spec), pool-properties))) diff --git a/src/metabase/driver/sql_jdbc/connection.clj b/src/metabase/driver/sql_jdbc/connection.clj index 581303dd0d3d58f8adbd0ff600d0c0099436cc80..5326b7eedabcbbb359ba93523c51405ff61c2e1c 100644 --- a/src/metabase/driver/sql_jdbc/connection.clj +++ b/src/metabase/driver/sql_jdbc/connection.clj @@ -79,7 +79,7 @@ [_ database] (set-pool! (u/get-id database) nil)) -(def ^:private db->pooled-spec-lock (Object.)) +(def ^:private create-pool-lock (Object.)) (defn db->pooled-connection-spec "Return a JDBC connection spec that includes a cp30 `ComboPooledDataSource`. @@ -92,7 +92,7 @@ ;; don't want to end up with a bunch of simultaneous threads creating pools only to have them destroyed the very ;; next instant. This will cause their queries to fail. Thus we should do the usual locking here and make sure only ;; one thread will be creating a pool at a given instant. - (locking db->pooled-spec-lock + (locking create-pool-lock (or ;; check if another thread created the pool while we were waiting to acquire the lock (get @database-id->connection-pool (u/get-id database-or-id)) diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj index 33b05b3667695a079f06f884a90bd0834caf34b3..4a0440abb89b325039ed447e8d34faa3e0082763 100644 --- a/src/metabase/email/messages.clj +++ b/src/metabase/email/messages.clj @@ -23,9 +23,8 @@ [toucan.db :as db]) (:import [java.io File IOException])) -(when-not *compile-files* - (when config/is-dev? - (alter-meta! #'stencil.core/render-file assoc :style/indent 1))) +(when config/is-dev? + (alter-meta! #'stencil.core/render-file assoc :style/indent 1)) ;; Dev only -- disable template caching (when config/is-dev? diff --git a/src/metabase/metabot/command.clj b/src/metabase/metabot/command.clj index 6206411b70e2967fa7fce079ada137b46413b550..8843e44ec04e4bf232b20610a5c6638eadd725c4 100644 --- a/src/metabase/metabot/command.clj +++ b/src/metabase/metabot/command.clj @@ -99,11 +99,11 @@ ;;; ------------------------------------------------------ list ------------------------------------------------------ -(defn- formar-cards-list +(defn- format-cards-list "Format a sequence of Cards as a nice multiline list for use in responses." [cards] - (apply str (interpose "\n" (for [{id :id, card-name :name} cards] - (format "%d. <%s|\"%s\">" id (urls/card-url id) card-name))))) + (str/join "\n" (for [{id :id, card-name :name} cards] + (format "%d. <%s|\"%s\">" id (urls/card-url id) card-name)))) (defn- list-cards [] (filter-metabot-readable @@ -127,7 +127,7 @@ (let [cards (list-cards)] (str (tru "Here''s your {0} most recent cards:" (count cards)) "\n" - (formar-cards-list cards)))) + (format-cards-list cards)))) ;;; ------------------------------------------------------ show ------------------------------------------------------ @@ -135,7 +135,9 @@ (defn- cards-with-name [card-name] (db/select [Card :id :name] :%lower.name [:like (str \% (str/lower-case card-name) \%)] - :archived false)) + :archived false + {:order-by [[:%lower.name :asc]] + :limit 10})) (defn- card-with-name [card-name] (let [[first-card & more, :as cards] (cards-with-name card-name)] @@ -145,7 +147,7 @@ (str (tru "Could you be a little more specific, or use the ID? I found these cards with names that matched:") "\n" - (formar-cards-list cards))))) + (format-cards-list cards))))) first-card)) (defn- id-or-name->card [card-id-or-name] diff --git a/src/metabase/query_processor/middleware/check_features.clj b/src/metabase/query_processor/middleware/check_features.clj index 2648c58a527187761b990d6c7228aca434f87785..f908b4316a9da1fc67932839e2b168d0176cdfcc 100644 --- a/src/metabase/query_processor/middleware/check_features.clj +++ b/src/metabase/query_processor/middleware/check_features.clj @@ -21,7 +21,9 @@ :stddev :standard-deviation-aggregations - :joined-field + ;; `:fk->` is normally replaced by `:joined-field` already but the middleware that does the replacement won't run + ;; if the driver doesn't support foreign keys, meaning the clauses can leak thru + #{:joined-field :fk->} :foreign-keys)) (defn- check-features* [{query-type :type, :as query}] diff --git a/src/metabase/query_processor/middleware/resolve_joined_tables.clj b/src/metabase/query_processor/middleware/resolve_joined_tables.clj index d3be4eb28882817036c0dc9778a397c49352fc58..7872ab3ba40bc57159411cf7d16404410b686f51 100644 --- a/src/metabase/query_processor/middleware/resolve_joined_tables.clj +++ b/src/metabase/query_processor/middleware/resolve_joined_tables.clj @@ -182,15 +182,18 @@ resolved-source-query (assoc-in [:query :source-query] resolved-source-query)))) (defn- resolve-joined-tables* [{query-type :type, :as query}] + ;; if this is a native query, or if `driver/*driver*` is bound *and* it DOES NOT support `:foreign-keys`, return + ;; query as is. Otherwise add implicit joins for `fk->` clauses (if (or (= query-type :native) - (and driver/*driver* - (not (driver/supports? driver/*driver* :foreign-keys)))) + (some-> driver/*driver* ((complement driver/supports?) :foreign-keys))) query (resolve-joined-tables-in-query-all-levels query))) (defn resolve-joined-tables "Fetch and store any Tables other than the source Table referred to by `fk->` clauses in an MBQL query, and add a - `:join-tables` key inside the MBQL inner query dictionary containing information about the `JOIN`s (or equivalent) - that need to be performed for these tables." + `:join-tables` key inside the MBQL inner query containing information about the `JOIN`s (or equivalent) that need to + be performed for these tables. + + This middleware also replaces all `fk->` clauses with `joined-field` clauses, which are easier to work with." [qp] (comp qp resolve-joined-tables*)) diff --git a/src/metabase/util.clj b/src/metabase/util.clj index 645c1046edc9e18bd7f3431b504f47a30cf989ae..501dc3dddfaccf5a1aa9673882c321f1854ef2a4 100644 --- a/src/metabase/util.clj +++ b/src/metabase/util.clj @@ -280,46 +280,36 @@ "Get the stack trace associated with E and return it as a vector with non-metabase frames after the last Metabase frame filtered out.")) -;; These next two functions are a workaround for this bug https://dev.clojure.org/jira/browse/CLJ-1790 -;; When Throwable/Thread are type-hinted, they return an array of type StackTraceElement, this causes -;; a VerifyError. Adding a layer of indirection here avoids the problem. Once we upgrade to Clojure 1.9 -;; we should be able to remove this code. -(defn- throwable-get-stack-trace [^Throwable t] - (.getStackTrace t)) +(extend-protocol IFilteredStacktrace + nil + (filtered-stacktrace [_] nil) -(defn- thread-get-stack-trace [^Thread t] - (.getStackTrace t)) + Throwable + (filtered-stacktrace [^Throwable this] + (filtered-stacktrace (.getStackTrace this))) -(extend nil - IFilteredStacktrace {:filtered-stacktrace (constantly nil)}) + Thread + (filtered-stacktrace [^Thread this] + (filtered-stacktrace (.getStackTrace this)))) -(extend Throwable - IFilteredStacktrace {:filtered-stacktrace (fn [this] - (filtered-stacktrace (throwable-get-stack-trace this)))}) - -(extend Thread - IFilteredStacktrace {:filtered-stacktrace (fn [this] - (filtered-stacktrace (thread-get-stack-trace this)))}) - -(defn- metabase-frame? [frame] - (re-find #"metabase" (str frame))) - -;; StackTraceElement[] is what the `.getStackTrace` method for Thread and Throwable returns (extend (Class/forName "[Ljava.lang.StackTraceElement;") IFilteredStacktrace {:filtered-stacktrace (fn [this] ;; keep all the frames before the last Metabase frame, but then filter out any other non-Metabase frames after ;; that - (let [[frames-after-last-mb other-frames] (split-with (complement metabase-frame?) - (map str (seq this))) - [last-mb-frame & frames-before-last-mb] (map #(str/replace % #"^metabase\." "") - (filter metabase-frame? other-frames))] + (let [[frames-after-last-mb other-frames] (split-with #(not (str/includes? % "metabase")) + (seq this)) + [last-mb-frame & frames-before-last-mb] (for [frame other-frames + :when (str/includes? frame "metabase")] + (str/replace frame #"^metabase\." ""))] (concat - frames-after-last-mb + (map str frames-after-last-mb) ;; add a little arrow to the frame so it stands out more - (cons (some->> last-mb-frame (str "--> ")) - frames-before-last-mb))))}) + (cons + (some->> last-mb-frame (str "--> ")) + frames-before-last-mb))))}) + (defn deref-with-timeout "Call `deref` on a something derefable (e.g. a future or promise), and throw an exception if it takes more than diff --git a/src/metabase/util/honeysql_extensions.clj b/src/metabase/util/honeysql_extensions.clj index c5909fe87fe14e0a90cf3ba0ea36a0f26aca0780..1ebd1787c4b971cd03e7b0fc71bdc006a87a0545 100644 --- a/src/metabase/util/honeysql_extensions.clj +++ b/src/metabase/util/honeysql_extensions.clj @@ -11,10 +11,9 @@ (:import honeysql.format.ToSql java.util.Locale)) -(when-not *compile-files* - (when config/is-dev? - (alter-meta! #'honeysql.core/format assoc :style/indent 1) - (alter-meta! #'honeysql.core/call assoc :style/indent 1))) +(when config/is-dev? + (alter-meta! #'honeysql.core/format assoc :style/indent 1) + (alter-meta! #'honeysql.core/call assoc :style/indent 1)) (defn- english-upper-case "Use this function when you need to upper-case an identifier or table name. Similar to `clojure.string/upper-case` @@ -78,6 +77,11 @@ (:components component) [component])] component))) +;; don't use `->Identifier` or `map->Identifier`. Use the `identifier` function instead, which cleans up its input +(when-not config/is-prod? + (alter-meta! #'->Identifier assoc :private true) + (alter-meta! #'map->Identifier assoc :private true)) + ;; Single-quoted string literal (defrecord Literal [literal] @@ -91,6 +95,11 @@ (pretty [_] (list 'literal literal))) +;; as with `Identifier` you should use the the `literal` function below instead of the auto-generated factory functions. +(when-not config/is-prod? + (alter-meta! #'->Literal assoc :private true) + (alter-meta! #'map->Literal assoc :private true)) + (defn literal "Wrap keyword or string `s` in single quotes and a HoneySQL `raw` form. @@ -107,8 +116,8 @@ (def ^{:arglists '([& exprs])} * "Math operator. Interpose `*` between `exprs` and wrap in parentheses." (partial hsql/call :*)) (def ^{:arglists '([& exprs])} mod "Math operator. Interpose `%` between `exprs` and wrap in parentheses." (partial hsql/call :%)) -(defn inc "Add 1 to X." [x] (+ x 1)) -(defn dec "Subtract 1 from X." [x] (- x 1)) +(defn inc "Add 1 to `x`." [x] (+ x 1)) +(defn dec "Subtract 1 from `x`." [x] (- x 1)) (defn cast @@ -134,13 +143,13 @@ [x decimal-places] (hsql/call :round x decimal-places)) -(defn ->date "CAST X to a `date`." [x] (cast :date x)) -(defn ->datetime "CAST X to a `datetime`." [x] (cast :datetime x)) -(defn ->timestamp "CAST X to a `timestamp`." [x] (cast :timestamp x)) -(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)) -(defn ->boolean "CAST X to a `boolean` datatype" [x] (cast :boolean x)) +(defn ->date "CAST `x` to a `date`." [x] (cast :date x)) +(defn ->datetime "CAST `x` to a `datetime`." [x] (cast :datetime x)) +(defn ->timestamp "CAST `x` to a `timestamp`." [x] (cast :timestamp x)) +(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)) +(defn ->boolean "CAST `x` to a `boolean` datatype" [x] (cast :boolean 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/util/pretty.clj b/src/metabase/util/pretty.clj index b756161b056f7c81cc63735f100e3c63ba448f63..da1988301fe417f86296e636ab1560642854279b 100644 --- a/src/metabase/util/pretty.clj +++ b/src/metabase/util/pretty.clj @@ -5,7 +5,9 @@ java.util.Map)) (defprotocol PrettyPrintable - "Implmement this protocol to return custom representations of objects when printing them." + "Implmement this protocol to return custom representations of objects when printing them. This only seems to work if + it's done as part of the type declaration (`defrecord`); it doesn't seem to be respected if you use + `extend-protocol` for an existing type. Not sure why this is :(" (pretty [_] "Return an appropriate representation of this object to be used when printing it, such as in the REPL or in log messages.")) diff --git a/test/expectation_options.clj b/test/expectation_options.clj index 56d3cafd493facfafa121043fe5726c4cc0b8a7d..d5865287234a8255fe93a4527ea486fe3cf72378 100644 --- a/test/expectation_options.clj +++ b/test/expectation_options.clj @@ -5,8 +5,7 @@ [set :as set]] [expectations :as expectations] [metabase.util :as u] - [metabase.util.date :as du]) - (:import java.util.concurrent.TimeoutException)) + [metabase.util.date :as du])) ;;; ---------------------------------------- Expectations Framework Settings ----------------------------------------- @@ -62,12 +61,14 @@ [run] (fn [test-fn] (let [start-time-ms (System/currentTimeMillis)] - (u/prog1 (run test-fn) - (let [duration-ms (- (System/currentTimeMillis) start-time-ms)] - (when (> duration-ms slow-test-threshold-ms) - (let [{:keys [file line]} (-> test-fn meta :the-var meta)] - (println (u/format-color 'red "%s %s is a slow test! It took %s to finish." - file line (du/format-milliseconds duration-ms)))))))))) + (run test-fn) + (let [duration-ms (- (System/currentTimeMillis) start-time-ms)] + (when (> duration-ms slow-test-threshold-ms) + (println + (let [{:keys [file line]} (-> test-fn meta :the-var meta)] + (u/format-color 'red "%s %s is a slow test! It took %s to finish." + file line (du/format-milliseconds duration-ms))) + "(This may have been because it was loading test data.)")))))) ;;; ------------------------------------------------ enforce-timeout ------------------------------------------------- @@ -75,18 +76,22 @@ (def ^:private test-timeout-ms (* 60 1000)) (defn- enforce-timeout - "If any test takes longer that 60 seconds to run return a TimeoutException, effectively failing the test." + "If any test takes longer that 60 seconds to run print a message and stop running tests. (This usually happens when + something is fundamentally broken, and we don't want to continue running thousands of tests that can hang for a + minute each.)" [run] (fn [test-fn] - (deref - (future - (try - (run test-fn) - (catch Throwable e - e))) - test-timeout-ms - ;; return Exception than throwing, otherwise it will mess up our test running - (TimeoutException. (format "Test timed out after %s" (du/format-milliseconds test-timeout-ms)))))) + (when (= (deref (future (run test-fn)) test-timeout-ms ::timed-out) + ::timed-out) + (let [{:keys [file line]} (-> test-fn meta :the-var meta)] + (println + (u/format-color 'red "%s %s timed out after %s" file line (du/format-milliseconds test-timeout-ms))) + (println "Stacktraces:") + (doseq [[thread stacktrace] (Thread/getAllStackTraces)] + (println "\n" (u/pprint-to-str 'red thread)) + (doseq [frame stacktrace] + (println frame)))) + (System/exit 1)))) ;;; ---------------------------------------------- check-table-cleanup ----------------------------------------------- @@ -101,12 +106,9 @@ message" [models-to-check] (for [model models-to-check - :let [instances-found (count (model)) - more-than-one? (> 1 instances-found)] - :when (< 0 instances-found)] - (str "Found '" instances-found "' instance" (when more-than-one? "s") - " of '" (:name model) "' that " (if more-than-one? "were" "was") - " not cleaned up."))) + :let [instances-found (count (model))] + :when (pos? instances-found)] + (format "Found %d instances of %s that were not cleaned up." instances-found (:name model)))) (def ^{:arglists '([run])} check-table-cleanup "Function that will run around each test. This function is usually a noop, but it useful for helping to debug stale @@ -117,34 +119,37 @@ identity (fn [run] (fn [test-fn] - (let [result (run test-fn) - {:keys [file line]} (-> test-fn meta :the-var meta) - error-msgs (tables-with-data->error-msg models-to-check)] + (run test-fn) + (let [error-msgs (tables-with-data->error-msg models-to-check)] (when (seq error-msgs) (println "\n-----------------------------------------------------") (doseq [error-msg error-msgs] (println error-msg)) (println "-----------------------------------------------------") - (printf "\nStale test rows found in tables, check '%s' at line '%s'\n\n" file line) + (let [{:keys [file line]} (-> test-fn meta :the-var meta)] + (printf "\nStale test rows found in tables, check '%s' at line '%s'\n\n" file line)) (flush) ;; I found this necessary as throwing an exception would show the exception, but the test run would hang and ;; you'd have to Ctrl-C anyway - (System/exit 1)) - result))))) + (System/exit 1))))))) ;;; -------------------------------------------- Putting it all together --------------------------------------------- (defn- log-tests [run] - (fn [test-fn] - (let [{:keys [file line]} (-> test-fn meta :the-var meta)] - (println (format "Run %s %s" file line))) - (run test-fn))) - + (comp + run + (fn [test-fn] + (let [{:keys [file line]} (-> test-fn meta :the-var meta)] + (println (format "Run %s %s" file line))) + test-fn))) + +;; This middleware does not need to worry about passing results to the next function in the chain; `test-fn` always +;; returns `nil` (def ^:private ^{:expectations-options :in-context} test-middleware (-> (fn [test-fn] (test-fn)) - ;; uncomment `log-tests` if you need to debug tests or see which ones are being noisy + ;; uncomment `log-tests` if you need to debug tests or see which ones are being noisy or hanging forever #_log-tests log-slow-tests enforce-timeout diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj index bde8888c4a81770ff3e0e719c5235a3f04ffcee2..68e5dee50a7295dde22d22c0e2b687c92c3c43a4 100644 --- a/test/metabase/query_processor_test.clj +++ b/test/metabase/query_processor_test.clj @@ -294,27 +294,36 @@ this behavior." {:style/indent 1} ([format-fns rows] - (format-rows-by format-fns (not :format-nil-values?) rows)) - ([format-fns format-nil-values? rows] - (cond - (= (:status rows) :failed) - (do (println "Error running query:" (u/pprint-to-str 'red rows)) - (throw (ex-info (:error rows) rows))) - - (:data rows) - (update-in rows [:data :rows] (partial format-rows-by format-fns)) - - (:rows rows) - (update rows :rows (partial format-rows-by format-fns)) - - :else - (vec (for [row rows] - (vec (for [[f v] (partition 2 (interleave format-fns row))] - (when (or v format-nil-values?) - (try (f v) - (catch Throwable e - (printf "(%s %s) failed: %s" f v (.getMessage e)) - (throw e))))))))))) + (format-rows-by format-fns false rows)) + + ([format-fns format-nil-values? response] + (when (= (:status response) :failed) + (println "Error running query:" (u/pprint-to-str 'red response)) + (throw (ex-info (:error response) response))) + + (-> response + ((fn format-rows [rows] + (cond + (:data rows) + (update rows :data format-rows) + + (:rows rows) + (update rows :rows format-rows) + + (sequential? rows) + (vec + (for [row rows] + (vec + (for [[f v] (partition 2 (interleave format-fns row))] + (when (or v format-nil-values?) + (try + (f v) + (catch Throwable e + (printf "(%s %s) failed: %s" f v (.getMessage e)) + (throw e)))))))) + + :else + (throw (ex-info "Unexpected response: rows are not sequential!" {:response response})))))))) (def ^{:arglists '([results])} formatted-venues-rows "Helper function to format the rows in `results` when running a 'raw data' query against the Venues test table."