Shared functions used by audit internal queries across different namespaces. | (ns metabase-enterprise.audit-app.pages.common (:require [clojure.core.memoize :as memoize] [clojure.walk :as walk] [honey.sql.helpers :as sql.helpers] [medley.core :as m] [metabase-enterprise.audit-app.query-processor.middleware.handle-audit-queries :as qp.middleware.audit] [metabase.db :as mdb] [metabase.db.query :as mdb.query] [metabase.driver.sql-jdbc.execute :as sql-jdbc.execute] [metabase.driver.sql-jdbc.sync :as sql-jdbc.sync] [metabase.query-processor.schema :as qp.schema] [metabase.query-processor.timezone :as qp.timezone] [metabase.util :as u] [metabase.util.honey-sql-2 :as h2x] [metabase.util.i18n :refer [tru]] [metabase.util.malli :as mu])) |
(set! *warn-on-reflection* true) | |
(def ^:private ^:const default-limit Integer/MAX_VALUE) | |
(defn- add-default-params [honeysql-query] (let [{:keys [limit offset]} qp.middleware.audit/*additional-query-params*] (if (and (nil? limit) (nil? offset)) honeysql-query (-> honeysql-query (update :limit (fn [query-limit] [:inline (or limit query-limit default-limit)])) (update :offset (fn [query-offset] [:inline (or offset query-offset 0)])))))) | |
(defn- inject-cte-body-into-from [from ctes] (vec (for [source from] (if (vector? source) (let [[source alias] source] [(ctes source source) alias]) (if (ctes source) [(ctes source) source] source))))) | |
(defn- inject-cte-body-into-join [joins ctes] (->> joins (partition 2) (mapcat (fn [[source condition]] (if (vector? source) (let [[source alias] source] [(if (ctes source) [(ctes source) alias] [source alias]) condition]) [(if (ctes source) [(ctes source) source] source) condition]))) vec)) | |
(defn- CTEs->subselects ([query] (CTEs->subselects query {})) ([{:keys [with] :as query} ctes] (let [ctes (reduce (fn [ctes [alias definition]] (assoc ctes alias (CTEs->subselects definition ctes))) ctes with)] (walk/postwalk (fn [form] (if (map? form) (-> form (m/update-existing :from inject-cte-body-into-from ctes) ;; TODO -- make this work with all types of joins (m/update-existing :left-join inject-cte-body-into-join ctes) (m/update-existing :join inject-cte-body-into-join ctes)) form)) (dissoc query :with))))) | |
(def ^:private ^{:arglists '([])} application-db-default-timezone ;; cache the application DB's default timezone for an hour. I don't expect this information to change *ever*, ;; really, but it seems like it is possible that it *could* change. Determining this for every audit query seems ;; wasteful however. (mdb/memoize-for-application-db (memoize/ttl (fn [] #_{:clj-kondo/ignore [:deprecated-var]} (sql-jdbc.sync/db-default-timezone (mdb/db-type) {:datasource (mdb/app-db)})) :ttl/threshold (u/hours->ms 1)))) | |
(defn- compile-honeysql [driver honeysql-query] (try (let [honeysql-query (cond-> honeysql-query ;; MySQL 5.x does not support CTEs, so convert them to subselects instead (= driver :mysql) CTEs->subselects)] (mdb.query/compile (add-default-params honeysql-query))) (catch Throwable e (throw (ex-info (tru "Error compiling audit query: {0}" (ex-message e)) {:driver driver, :honeysql-query honeysql-query} e))))) | |
(mu/defn- reduce-results* :- :some [honeysql-query :- :map rff :- ::qp.schema/rff init] (let [driver (mdb/db-type) [sql & params] (compile-honeysql driver honeysql-query)] ;; MySQL driver normalizies timestamps. Setting `*results-timezone-id-override*` is a shortcut ;; instead of mocking up a chunk of regular QP pipeline. (binding [qp.timezone/*results-timezone-id-override* (application-db-default-timezone)] (try (with-open [conn (.getConnection (mdb/app-db)) stmt (sql-jdbc.execute/prepared-statement driver conn sql params) rs (sql-jdbc.execute/execute-prepared-statement! driver stmt)] (let [rsmeta (.getMetaData rs) cols (for [col (sql-jdbc.execute/column-metadata driver rsmeta)] (update col :name u/lower-case-en)) metadata {:cols cols} rf (rff metadata)] (reduce rf init (sql-jdbc.execute/reducible-rows driver rs rsmeta)))) (catch Throwable e (throw (ex-info (tru "Error running audit query: {0}" (ex-message e)) {:driver driver :honeysql-query honeysql-query :sql sql :params params} e))))))) | |
Return a function with the signature (thunk) -> IReduceInit that, when reduced, runs | (defn reducible-query [honeysql-query] (bound-fn reducible-query-thunk [] (reify clojure.lang.IReduceInit (reduce [_this rf init] (reduce-results* honeysql-query (constantly rf) init))))) |
Run a internal audit query, automatically including limits and offsets for paging. This function returns results
directly as a series of maps (the 'legacy results' format as described in
| (defn query [honeysql-query] (let [rff (fn rff [{:keys [cols]}] (let [col-names (mapv (comp keyword :name) cols)] ((map (partial zipmap col-names)) conj)))] (reduce-results* honeysql-query rff []))) |
+----------------------------------------------------------------------------------------------------------------+ | Helper Fns | +----------------------------------------------------------------------------------------------------------------+ | |
HoneySQL to grab the full name of a User. (user-full-name :u) ;; -> 'Cam Saul' | (defn user-full-name [user-table] (let [first-name (keyword (name user-table) "first_name") last-name (keyword (name user-table) "last_name") email (keyword (name user-table) "email")] [:case [:and [:= nil first-name] [:= nil last-name]] email [:or [:= nil first-name] [:= nil last-name]] (h2x/concat [:coalesce first-name ""] [:coalesce last-name ""]) :else (h2x/concat [:coalesce first-name ""] (h2x/literal " ") [:coalesce last-name ""])])) |
Lowercase a SQL field, to enter into honeysql query | (defn lowercase-field [field] [:lower field]) |
Add an appropriate (add-search-clause {} "birds" :t.name :db.name) | (defn add-search-clause [query query-string & fields-to-search] (sql.helpers/where query (when (seq query-string) (let [query-string (str \% (u/lower-case-en query-string) \%)] (cons :or (for [field fields-to-search] [:like (lowercase-field field) query-string])))))) |
Add an Most queries will just have explicit default | (defn add-sort-clause [query sort-column sort-direction] (sql.helpers/order-by query [(keyword sort-column) (keyword sort-direction)])) |