diff --git a/src/metabase/db.clj b/src/metabase/db.clj index bb9602233d8f85e22342a824ce934d1a58c2f8cb..f760ae191c44d3703006773bd36c07be97318285 100644 --- a/src/metabase/db.clj +++ b/src/metabase/db.clj @@ -2,7 +2,8 @@ "Korma database definition and helper functions for interacting with the database." (:require [clojure.java.jdbc :as jdbc] [clojure.tools.logging :as log] - [clojure.string :as str] + (clojure [set :as set] + [string :as str]) [environ.core :refer [env]] (korma [core :refer :all] [db :refer :all]) @@ -131,63 +132,7 @@ (apply setup-db args))) -;; # UTILITY FUNCTIONS - -;; ## CAST-COLUMNS - -;; TODO - Doesn't Korma have similar `transformations` functionality? Investigate. - -(def ^:const ^:private type-fns - "A map of column type keywords to the functions that should be used to \"cast\" - them when going `:in` or `:out` of the database." - {:json {:in i/write-json - :out i/read-json} - :keyword {:in name - :out keyword}}) - -(defn types - "Tag columns in an entity definition with a type keyword. - This keyword will be used to automatically \"cast\" columns when they are present. - - ;; apply ((type-fns :json) :in) -- cheshire/generate-string -- to value of :details before inserting into DB - ;; apply ((type-fns :json) :out) -- read-json -- to value of :details when reading from DB - (defentity Database - (types {:details :json}))" - [entity types-map] - {:pre [(every? keyword? (keys types-map)) - (every? (partial contains? type-fns) (vals types-map))]} - (assoc entity ::types types-map)) - -(defn apply-type-fns - "Recursively apply a sequence of functions associated with COLUMN-TYPE-PAIRS to OBJ. - - COLUMN-TYPE-PAIRS should be the value of `(seq (::types korma-entity))`. - DIRECTION should be either `:in` or `:out`." - {:arglists '([direction column-type-pairs obj])} - [direction [[column column-type] & rest-pairs] obj] - (if-not column obj - (recur direction rest-pairs (if-not (column obj) obj - (update-in obj [column] (-> type-fns column-type direction)))))) - -;; TODO - It would be good to allow custom types by just inserting the `{:in fn :out fn}` inline with the -;; entity definition - -;; TODO - hydration-keys should be an entity function for the sake of prettiness - - -;; ## TIMESTAMPED - -(defn timestamped - "Mark ENTITY as having `:created_at` *and* `:updated_at` fields. - - (defentity Card - timestamped) - - * When a new object is created via `ins`, values for both fields will be generated. - * When an object is updated via `upd`, `:updated_at` will be updated." - [entity] - (assoc entity ::timestamped true)) - +;; # ---------------------------------------- UTILITY FUNCTIONS ---------------------------------------- ;; ## UPD @@ -202,10 +147,8 @@ {:pre [(integer? entity-id)]} (let [obj (->> (assoc kwargs :id entity-id) (models/pre-update entity) - (#(dissoc % :id)) - (apply-type-fns :in (seq (::types entity)))) - obj (cond-> obj - (::timestamped entity) (assoc :updated_at (u/new-sql-timestamp))) + (models/internal-pre-update entity) + (#(dissoc % :id))) result (-> (update entity (set-fields obj) (where {:id entity-id})) (> 0))] (when result @@ -335,7 +278,6 @@ (defn -sel-transform [entity result] (->> result (models/internal-post-select entity) - #_(apply-type-fns :out (seq (::types entity))) (models/post-select entity))) (defmacro -sel-select @@ -368,14 +310,10 @@ [entity & {:as kwargs}] (let [vals (->> kwargs (models/pre-insert entity) - (apply-type-fns :in (seq (::types entity)))) - vals (cond-> vals - (::timestamped entity) (assoc :created_at (u/new-sql-timestamp) - :updated_at (u/new-sql-timestamp))) + (models/internal-pre-insert entity)) {:keys [id]} (-> (insert entity (values vals)) - (clojure.set/rename-keys {(keyword "scope_identity()") :id}))] - (->> (sel :one entity :id id) - (models/post-insert entity)))) + (set/rename-keys {(keyword "scope_identity()") :id}))] + (models/post-insert entity (entity id)))) ;; ## EXISTS? diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj index 215cca55e01283634f46433d6495b85593496569..86e91a60aa380586bd6cd31d77e223c0d427679f 100644 --- a/src/metabase/models/card.clj +++ b/src/metabase/models/card.clj @@ -21,11 +21,9 @@ (defentity Card [(table :report_card) - (types {:dataset_query :json - :display :keyword - :visualization_settings :json}) - timestamped - (assoc :hydration-keys #{:card})] + (hydration-keys card) + (types :dataset_query :json, :display :keyword, :visualization_settings :json) + timestamped] (post-select [_ {:keys [creator_id] :as card}] (-> (assoc card diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj index 03eb3b8c45b9943f95f3c012e893f90f0826d5b1..2709fdb72a261d6dcf3461c9dd718a61e8967ebd 100644 --- a/src/metabase/models/database.clj +++ b/src/metabase/models/database.clj @@ -19,11 +19,10 @@ (defentity Database [(table :metabase_database) - (types {:details :json - :engine :keyword}) - timestamped - (assoc :hydration-keys #{:database - :db})] + (hydration-keys database db) + (types :details :json, :engine :keyword) + timestamped] + (post-select [_ db] (map->DatabaseInstance (assoc db diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj index a6495b5eea4f21e723c23cd8acfdaf8367167fbb..9417e5a2d98205f2bf4223cd383de7f4d6f1defe 100644 --- a/src/metabase/models/field.clj +++ b/src/metabase/models/field.clj @@ -76,11 +76,9 @@ (defentity Field [(table :metabase_field) - timestamped - (types {:base_type :keyword - :field_type :keyword - :special_type :keyword}) - (hydration-keys destination field origin)] + (hydration-keys destination field origin) + (types :base_type :keyword, :field_type :keyword, :special_type :keyword) + timestamped] (pre-insert [_ field] (let [defaults {:active true diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj index 9537c8068d314d243f342c8311a2488782389ed0..e6c25f51ea83668e05a8ef0e07a98901d7dcdbe2 100644 --- a/src/metabase/models/field_values.clj +++ b/src/metabase/models/field_values.clj @@ -10,8 +10,7 @@ (defentity FieldValues [(table :metabase_fieldvalues) timestamped - (types {:human_readable_values :json - :values :json})] + (types :human_readable_values :json, :values :json)] (post-select [_ field-values] (update-in field-values [:human_readable_values] #(or % {})))) diff --git a/src/metabase/models/foreign_key.clj b/src/metabase/models/foreign_key.clj index 4737681b5bc3595647467c8b58beb3657055757a..05b4ad25446fc001686e5831b80102b0e746e6a6 100644 --- a/src/metabase/models/foreign_key.clj +++ b/src/metabase/models/foreign_key.clj @@ -16,8 +16,8 @@ (defentity ForeignKey [(table :metabase_foreignkey) - timestamped - (types {:relationship :keyword})] + (types :relationship :keyword) + timestamped] (post-select [_ {:keys [origin_id destination_id] :as fk}] (assoc fk diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index 18f36b499db7c655b55760e27901d80a674cb1e4..c6300681e0371f96b1a2cdf74c2a598c96927526 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -4,7 +4,8 @@ [clojure.walk :refer [macroexpand-all]] [korma.core :as k] [medley.core :as m] - [metabase.config :as config])) + [metabase.config :as config] + [metabase.util :as u])) ;;; ## ---------------------------------------- ENTITIES ---------------------------------------- @@ -33,8 +34,6 @@ The output of this function is ignored.") - (internal-post-select [this instance]) - (post-select [this instance] "Called on the results from a call to `sel`. Default implementation doesn't do anything, but you can provide custom implementations to do things like add hydrateable keys or remove sensitive fields.") @@ -48,21 +47,22 @@ (pre-cascade-delete [_ {database-id :id :as database}] (cascade-delete Card :database_id database-id) - ...)")) + ...)") + + (internal-pre-insert [this instance]) + (internal-pre-update [this instance]) + (internal-post-select [this instance])) -(defn- identity-second - "Return the second arg as-is." - [_ obj] - obj) +(defn- identity-second [_ obj] obj) +(def ^:private constantly-nil (constantly nil)) (def ^:const ^:private default-entity-method-implementations - {:pre-insert identity-second - :post-insert identity-second - :pre-update identity-second - :post-update '(constantly nil) - :post-select identity-second - :pre-cascade-delete '(constantly nil) - :internal-post-select identity-second}) + {:pre-insert #'identity-second + :post-insert #'identity-second + :pre-update #'identity-second + :post-update #'constantly-nil + :post-select #'identity-second + :pre-cascade-delete #'constantly-nil}) (def ^:const ^:private type-fns {:json {:in 'metabase.db.internal/write-json @@ -70,8 +70,16 @@ :keyword {:in `name :out `keyword}}) -(defn- resolve-type-fns [types-map] - (m/map-vals #(:out (type-fns %)) types-map)) +(defmacro apply-type-fns [obj-binding direction entity-map] + {:pre [(symbol? obj-binding) + (keyword? direction) + (map? entity-map)]} + (let [fns (m/map-vals #(direction (type-fns %)) (::types entity-map))] + (if-not (seq fns) obj-binding + `(cond-> ~obj-binding + ~@(mapcat (fn [[k f]] + [`(~k ~obj-binding) `(update-in [~k] ~f)]) + fns))))) (defn -invoke-entity [entity id] (future @@ -86,15 +94,18 @@ (internal-post-select entity) (post-select entity))))) -(defmacro make-internal-post-select [obj kvs] - `(cond-> ~obj - ~@(mapcat (fn [[k f]] - [`(~k ~obj) `(update-in [~k] ~f)]) - (seq kvs)))) +(defn- update-updated-at [obj] + (assoc obj :updated_at (u/new-sql-timestamp))) + +(defn- update-created-at-updated-at [obj] + (let [ts (u/new-sql-timestamp)] + (assoc obj :created_at ts, :updated_at ts))) (defmacro macrolet-entity-map [entity & entity-forms] - `(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(map keyword fields#)])) - (~'hydration-keys [m# & fields#] `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))] + `(macrolet [(~'default-fields [m# & fields#] `(assoc ~m# ::default-fields [~@(map keyword fields#)])) + (~'timestamped [m#] `(assoc ~m# ::timestamped true)) + (~'types [m# & {:as fields#}] `(assoc ~m# ::types ~fields#)) + (~'hydration-keys [m# & fields#] `(assoc ~m# :hydration-keys #{~@(map keyword fields#)}))] (-> (k/create-entity ~(name entity)) ~@entity-forms))) @@ -105,8 +116,7 @@ (every? list? methods)]} (let [entity-symb (symbol (format "%sEntity" (name entity))) internal-post-select-symb (symbol (format "internal-post-select-%s" (name entity))) - entity-map (eval `(macrolet-entity-map ~entity ~@entity-forms)) - type-fns (resolve-type-fns (:metabase.db/types entity-map))] + entity-map (eval `(macrolet-entity-map ~entity ~@entity-forms))] `(do (defrecord ~entity-symb [] clojure.lang.IFn @@ -115,13 +125,20 @@ (extend ~entity-symb IEntity ~(merge default-entity-method-implementations - {:internal-post-select `(fn [~'_ ~'obj] - ~(macroexpand-1 `(make-internal-post-select ~'obj ~type-fns)))} + {:internal-pre-insert `(fn [~'_ obj#] + (-> (apply-type-fns obj# :in ~entity-map) + ~@(when (::timestamped entity-map) + [update-created-at-updated-at]))) + :internal-pre-update `(fn [~'_ obj#] + (-> (apply-type-fns obj# :in ~entity-map) + ~@(when (::timestamped entity-map) + [update-updated-at]))) + :internal-post-select `(fn [~'_ obj#] + (apply-type-fns obj# :out ~entity-map))} (into {} (for [[method-name & impl] methods] {(keyword method-name) `(fn ~@impl)})))) - - (def ~entity ; ~(vary-meta entity assoc :const true) + (def ~entity (~(symbol (format "map->%sEntity" (name entity))) ~entity-map))))) diff --git a/src/metabase/models/query_execution.clj b/src/metabase/models/query_execution.clj index 99d408e0ac8dd3fbb75c4a5eb6393cd4f8c8473b..0599fa0977c9691e97f5c7ddf111f1d739d3d6a5 100644 --- a/src/metabase/models/query_execution.clj +++ b/src/metabase/models/query_execution.clj @@ -10,9 +10,7 @@ (defentity QueryExecution [(table :query_queryexecution) (default-fields id uuid version json_query raw_query status started_at finished_at running_time error result_rows) - (types {:json_query :json - :result_data :json - :status :keyword})] + (types :json_query :json, :result_data :json, :status :keyword)] (post-select [_ {:keys [result_rows] :as query-execution}] ;; sadly we have 2 ways to reference the row count :( diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj index a550536a7eee777ebe2a91d81681d2f530af6b75..8978266fdce1ddeb01cfa7646fbc7995058caf0d 100644 --- a/src/metabase/models/table.clj +++ b/src/metabase/models/table.clj @@ -14,9 +14,9 @@ (defentity Table [(table :metabase_table) - timestamped (hydration-keys table) - (types {:entity_type :keyword})] + (types :entity_type :keyword) + timestamped] (post-select [_ {:keys [id db db_id description] :as table}] (u/assoc* table