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)])) |