diff --git a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj index 4671373806c10a6b455861b3151826ade5129774..4ef1e632a4cbb938b373dbf5e0562ae1b731d526 100644 --- a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj +++ b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj @@ -79,3 +79,9 @@ (is (= true (-> (.newInstance model) toucan.models/properties :entity_id)))))) + +(deftest comprehensive-identity-hash-test + (doseq [model (->> (extenders IModel) + (remove entities-not-exported))] + (testing (format "Model %s should implement IdentityHashable" (.getSimpleName model)) + (is (extends? metabase.models.serialization.hash/IdentityHashable model))))) diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj index 40c57d6a88a53894f319ccdcbac282ada4e12671..045c1a2fbf4fe16c2dc407410ad45555fd962e29 100644 --- a/src/metabase/models/card.clj +++ b/src/metabase/models/card.clj @@ -13,6 +13,7 @@ [metabase.models.permissions :as perms] [metabase.models.query :as query] [metabase.models.revision :as revision] + [metabase.models.serialization.hash :as serdes.hash] [metabase.moderation :as moderation] [metabase.plugins.classloader :as classloader] [metabase.public-settings :as public-settings] @@ -320,4 +321,7 @@ :serialize-instance serialize-instance) dependency/IDependent - {:dependencies card-dependencies}) + {:dependencies card-dependencies} + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :collection)])}) diff --git a/src/metabase/models/collection.clj b/src/metabase/models/collection.clj index d323eee5c1e31d1b6cf6f879ab9405e46b30646b..f6724c36d40c2caef956d711a507c57b17065173 100644 --- a/src/metabase/models/collection.clj +++ b/src/metabase/models/collection.clj @@ -14,6 +14,7 @@ [metabase.models.collection.root :as collection.root] [metabase.models.interface :as mi] [metabase.models.permissions :as perms :refer [Permissions]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.public-settings.premium-features :as premium-features] [metabase.util :as u] [metabase.util.honeysql-extensions :as hx] @@ -876,6 +877,14 @@ :read (perms/collection-read-path collection-or-id) :write (perms/collection-readwrite-path collection-or-id))}))) +(defn- parent-identity-hash [coll] + (let [parent-id (-> coll + (hydrate :parent_id) + :parent_id)] + (if parent-id + (serdes.hash/identity-hash (Collection parent-id)) + "ROOT"))) + (u/strict-extend (class Collection) models/IModel (merge models/IModelDefaults @@ -891,7 +900,10 @@ (merge mi/IObjectPermissionsDefaults {:can-read? (partial mi/current-user-has-full-permissions? :read) :can-write? (partial mi/current-user-has-full-permissions? :write) - :perms-objects-set perms-objects-set})) + :perms-objects-set perms-objects-set}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name :namespace parent-identity-hash])}) ;;; +----------------------------------------------------------------------------------------------------------------+ diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj index 20cbfd7537fea33472570af60c42dea117569936..edb521ec9aa54c47520878b8a8859250ad33f4dc 100644 --- a/src/metabase/models/dashboard.clj +++ b/src/metabase/models/dashboard.clj @@ -17,6 +17,7 @@ [metabase.models.pulse-card :as pulse-card :refer [PulseCard]] [metabase.models.revision :as revision] [metabase.models.revision.diff :refer [build-sentence]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.moderation :as moderation] [metabase.public-settings :as public-settings] [metabase.query-processor.async :as qp.async] @@ -140,7 +141,10 @@ ;; You can read/write a Dashboard if you can read/write its parent Collection mi/IObjectPermissions - perms/IObjectPermissionsForParentCollection) + perms/IObjectPermissionsForParentCollection + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :collection)])}) ;;; --------------------------------------------------- Revisions ---------------------------------------------------- diff --git a/src/metabase/models/dashboard_card.clj b/src/metabase/models/dashboard_card.clj index 4eeee60dc00c031fdcb6f1e984a1705893331313..9172092214de5481583b9564cb5b86685194c492 100644 --- a/src/metabase/models/dashboard_card.clj +++ b/src/metabase/models/dashboard_card.clj @@ -6,6 +6,7 @@ [metabase.models.dashboard-card-series :refer [DashboardCardSeries]] [metabase.models.interface :as mi] [metabase.models.pulse-card :refer [PulseCard]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [metabase.util.schema :as su] [schema.core :as s] @@ -48,7 +49,14 @@ (merge mi/IObjectPermissionsDefaults {:perms-objects-set perms-objects-set :can-read? (partial mi/current-user-has-full-permissions? :read) - :can-write? (partial mi/current-user-has-full-permissions? :write)})) + :can-write? (partial mi/current-user-has-full-permissions? :write)}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [(serdes.hash/hydrated-hash :card) + (comp serdes.hash/identity-hash + #(db/select-one 'Dashboard :id %) + :dashboard_id) + :visualization_settings])}) ;;; --------------------------------------------------- HYDRATION ---------------------------------------------------- @@ -59,7 +67,6 @@ {:pre [(integer? dashboard_id)]} (db/select-one 'Dashboard, :id dashboard_id)) - (defn ^:hydrate series "Return the `Cards` associated as additional series on this DashboardCard." [{:keys [id]}] diff --git a/src/metabase/models/dashboard_card_series.clj b/src/metabase/models/dashboard_card_series.clj index 4fd9072034feda457e220c80673a315a2feed6ce..3d5ff22e05f0de2555f9d80e5408d4d8dca6023e 100644 --- a/src/metabase/models/dashboard_card_series.clj +++ b/src/metabase/models/dashboard_card_series.clj @@ -1,4 +1,15 @@ (ns metabase.models.dashboard-card-series - (:require [toucan.models :as models])) + (:require [metabase.models.serialization.hash :as serdes.hash] + [metabase.util :as u] + [toucan.db :as db] + [toucan.models :as models])) (models/defmodel DashboardCardSeries :dashboardcard_series) + +(defn- dashboard-card [{:keys [dashboardcard_id]}] + (db/select-one 'DashboardCard :id dashboardcard_id)) + +(u/strict-extend (class DashboardCardSeries) + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [(comp serdes.hash/identity-hash dashboard-card) + (serdes.hash/hydrated-hash :card)])}) diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj index 827797884d5c369b7860cacbf748dde1fdc44681..60458040d4a6086b57ccc20a0074b5e857b116f5 100644 --- a/src/metabase/models/database.clj +++ b/src/metabase/models/database.clj @@ -10,6 +10,7 @@ [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] [metabase.models.secret :as secret :refer [Secret]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.plugins.classloader :as classloader] [metabase.util :as u] [metabase.util.i18n :refer [trs]] @@ -208,7 +209,10 @@ (merge mi/IObjectPermissionsDefaults {:perms-objects-set perms-objects-set :can-read? (partial mi/current-user-has-partial-permissions? :read) - :can-write? (partial mi/current-user-has-full-permissions? :write)})) + :can-write? (partial mi/current-user-has-full-permissions? :write)}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name :engine])}) ;;; ---------------------------------------------- Hydration / Util Fns ---------------------------------------------- diff --git a/src/metabase/models/dependency.clj b/src/metabase/models/dependency.clj index 5ecec559354142b5cdc8101655ade4525cbfad11..085efc41f65396a3b8e69295f7e90333ca3ea122 100644 --- a/src/metabase/models/dependency.clj +++ b/src/metabase/models/dependency.clj @@ -3,6 +3,8 @@ example, a Card might use a Segment; a Dependency object will be used to track this dependency so appropriate actions can take place or be prevented when something changes." (:require [clojure.set :as set] + [metabase.models.serialization.hash :as serdes.hash] + [metabase.util :as u] [potemkin.types :as p.types] [toucan.db :as db] [toucan.models :as models])) @@ -20,6 +22,14 @@ (models/defmodel Dependency :dependency) +(defn- dependency-hash [{:keys [model model_id dependent_on_model dependent_on_id]}] + [model (serdes.hash/identity-hash (db/select-one (symbol model) :id model_id)) + dependent_on_model (serdes.hash/identity-hash (db/select-one (symbol dependent_on_model) :id dependent_on_id))]) + +(u/strict-extend (class Dependency) + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [dependency-hash])}) + (defn retrieve-dependencies "Get the list of dependencies for a given object." [entity id] diff --git a/src/metabase/models/dimension.clj b/src/metabase/models/dimension.clj index e9bd0df7628d31f5c5ea91283777f8bc46a996c7..80652a224df607c6058ed2883ea39f46179d0e89 100644 --- a/src/metabase/models/dimension.clj +++ b/src/metabase/models/dimension.clj @@ -2,7 +2,8 @@ "Dimensions are used to define remappings for Fields handled automatically when those Fields are encountered by the Query Processor. For a more detailed explanation, refer to the documentation in `metabase.query-processor.middleware.add-dimension-projections`." - (:require [metabase.util :as u] + (:require [metabase.models.serialization.hash :as serdes.hash] + [metabase.util :as u] [toucan.models :as models])) (def dimension-types @@ -17,4 +18,8 @@ (merge models/IModelDefaults {:types (constantly {:type :keyword}) :properties (constantly {:timestamped? true - :entity_id true})})) + :entity_id true})}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [(serdes.hash/hydrated-hash :field) + (serdes.hash/hydrated-hash :human_readable_field)])}) diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj index a4a0336f650526051f93bdee318709d35167b41a..8a247f369010788bbb8acd5e3816d14d64baec21 100644 --- a/src/metabase/models/field.clj +++ b/src/metabase/models/field.clj @@ -9,6 +9,7 @@ [metabase.models.humanization :as humanization] [metabase.models.interface :as mi] [metabase.models.permissions :as perms] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [metabase.util.honeysql-extensions :as hx] [metabase.util.i18n :refer [trs tru]] @@ -193,7 +194,10 @@ (merge mi/IObjectPermissionsDefaults {:perms-objects-set perms-objects-set :can-read? (partial mi/current-user-has-partial-permissions? :read) - :can-write? (partial mi/current-user-has-full-permissions? :write)})) + :can-write? (partial mi/current-user-has-full-permissions? :write)}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :table)])}) ;;; ---------------------------------------------- Hydration / Util Fns ---------------------------------------------- diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj index b623d041fbaadc994672f76a9f35c159ce0dd333..5c19f92a6eb195c7898ca0eeac9295950018b611 100644 --- a/src/metabase/models/field_values.clj +++ b/src/metabase/models/field_values.clj @@ -1,5 +1,6 @@ (ns metabase.models.field-values (:require [clojure.tools.logging :as log] + [metabase.models.serialization.hash :as serdes.hash] [metabase.plugins.classloader :as classloader] [metabase.util :as u] [metabase.util.i18n :refer [trs tru]] @@ -75,7 +76,10 @@ :types (constantly {:human_readable_values :json-no-keywordization, :values :json}) :pre-insert pre-insert :pre-update pre-update - :post-select post-select})) + :post-select post-select}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [(serdes.hash/hydrated-hash :field)])}) ;; ## FieldValues Helper Functions diff --git a/src/metabase/models/metric.clj b/src/metabase/models/metric.clj index 5948d10991bbc594979f885c49a53f4c52a28d58..d7a85f513c47ba5a63c1783af05f385bc0aecb88 100644 --- a/src/metabase/models/metric.clj +++ b/src/metabase/models/metric.clj @@ -7,6 +7,7 @@ [metabase.models.dependency :as dependency :refer [Dependency]] [metabase.models.interface :as mi] [metabase.models.revision :as revision] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.schema :as su] @@ -49,7 +50,10 @@ ;; for the time being you need to be a superuser in order to create or update Metrics because the UI for doing so ;; is only exposed in the admin panel :can-write? mi/superuser? - :can-create? mi/superuser?})) + :can-create? mi/superuser?}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :table)])}) ;;; --------------------------------------------------- REVISIONS ---------------------------------------------------- diff --git a/src/metabase/models/native_query_snippet.clj b/src/metabase/models/native_query_snippet.clj index 98bdc39446a2d4b91b29251264039f07e99152df..10a38e1b7a84e5e5ef40a2383727ced5bf303b87 100644 --- a/src/metabase/models/native_query_snippet.clj +++ b/src/metabase/models/native_query_snippet.clj @@ -2,6 +2,7 @@ (:require [metabase.models.collection :as collection] [metabase.models.interface :as mi] [metabase.models.native-query-snippet.permissions :as snippet.perms] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [metabase.util.i18n :refer [deferred-tru tru]] [metabase.util.schema :as su] @@ -44,7 +45,10 @@ {:can-read? snippet.perms/can-read? :can-write? snippet.perms/can-write? :can-create? snippet.perms/can-create? - :can-update? snippet.perms/can-update?})) + :can-update? snippet.perms/can-update?}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :collection)])}) ;;; ---------------------------------------------------- Schemas ----------------------------------------------------- diff --git a/src/metabase/models/pulse.clj b/src/metabase/models/pulse.clj index 6dfd2a62d0fbca3a590505bc2568477000e42cde..2051f8d1460398ed82b18ce0a6b19a06cf94ff11 100644 --- a/src/metabase/models/pulse.clj +++ b/src/metabase/models/pulse.clj @@ -26,6 +26,7 @@ [metabase.models.pulse-card :refer [PulseCard]] [metabase.models.pulse-channel :as pulse-channel :refer [PulseChannel]] [metabase.models.pulse-channel-recipient :refer [PulseChannelRecipient]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [metabase.util.i18n :refer [deferred-tru tru]] [metabase.util.schema :as su] @@ -134,7 +135,10 @@ mi/IObjectPermissionsDefaults {:can-read? (partial mi/current-user-has-full-permissions? :read) :can-write? can-write? - :perms-objects-set perms-objects-set})) + :perms-objects-set perms-objects-set}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :collection)])}) (def ^:private ^:dynamic *automatically-archive-when-last-channel-is-deleted* "Should we automatically archive a Pulse when its last `PulseChannel` is deleted? Normally we do, but this is disabled diff --git a/src/metabase/models/pulse_card.clj b/src/metabase/models/pulse_card.clj index 57ec05412b4cb6274d83550a9395fe4cef575e9c..fd73851bf58455f5612e1730fc1ae494632411cb 100644 --- a/src/metabase/models/pulse_card.clj +++ b/src/metabase/models/pulse_card.clj @@ -1,5 +1,6 @@ (ns metabase.models.pulse-card - (:require [metabase.util :as u] + (:require [metabase.models.serialization.hash :as serdes.hash] + [metabase.util :as u] [metabase.util.schema :as su] [schema.core :as s] [toucan.db :as db] @@ -7,6 +8,10 @@ (models/defmodel PulseCard :pulse_card) +(u/strict-extend (class PulseCard) + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [(serdes.hash/hydrated-hash :pulse) (serdes.hash/hydrated-hash :card)])}) + (defn next-position-for "Return the next available `pulse_card.position` for the given `pulse`" [pulse-id] diff --git a/src/metabase/models/pulse_channel.clj b/src/metabase/models/pulse_channel.clj index 7ddc62124c63fb49b2cc7deeae4e16e365197b0c..a48e959097ff8b640a96ee78ddb4e0a4f9c6b972 100644 --- a/src/metabase/models/pulse_channel.clj +++ b/src/metabase/models/pulse_channel.clj @@ -4,6 +4,7 @@ [medley.core :as m] [metabase.models.interface :as mi] [metabase.models.pulse-channel-recipient :refer [PulseChannelRecipient]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.user :as user :refer [User]] [metabase.plugins.classloader :as classloader] [metabase.util :as u] @@ -192,7 +193,10 @@ (merge mi/IObjectPermissionsDefaults {:can-read? (constantly true) - :can-write? mi/superuser?})) + :can-write? mi/superuser?}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [(serdes.hash/hydrated-hash :pulse) :channel_type :details])}) (defn will-delete-recipient "This function is called by [[metabase.models.pulse-channel-recipient/pre-delete]] when a `PulseChannelRecipient` is diff --git a/src/metabase/models/segment.clj b/src/metabase/models/segment.clj index df8210474513fe97ebbaf9588f6c6613d8baf382..15e410ade43a1b9c3ce99c8c9fc5a8954d7f14e3 100644 --- a/src/metabase/models/segment.clj +++ b/src/metabase/models/segment.clj @@ -4,6 +4,7 @@ (:require [medley.core :as m] [metabase.models.interface :as mi] [metabase.models.revision :as revision] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.schema :as su] @@ -43,7 +44,10 @@ ;; for the time being you need to be a superuser in order to create or update Segments because the UI for ;; doing so is only exposed in the admin panel :can-write? mi/superuser? - :can-create? mi/superuser?})) + :can-create? mi/superuser?}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :table)])}) ;;; --------------------------------------------------- Revisions ---------------------------------------------------- diff --git a/src/metabase/models/serialization/hash.clj b/src/metabase/models/serialization/hash.clj new file mode 100644 index 0000000000000000000000000000000000000000..d15f8bb1ac1527e61b04555ba1b876f008881db1 --- /dev/null +++ b/src/metabase/models/serialization/hash.clj @@ -0,0 +1,67 @@ +(ns metabase.models.serialization.hash + "Defines several helper functions and protocols for the serialization system. + Serialization is an enterprise feature, but in the interest of keeping all the code for an entity in one place, these + methods are defined here and implemented for all the exported models. + + Whether to export a new model: + - Generally, the high-profile user facing things (databases, questions, dashboards, snippets, etc.) are exported. + - Internal or automatic things (users, activity logs, permissions) are not. + + If the model is not exported, add it to the exclusion lists in the tests. + + For models that are exported, you have to implement this file's protocols and multimethods for it: + - All exported models should either have an CHAR(21) column `entity_id`, or a portable external name (like a database + URL). + - [[identity-hash-fields]] should give the list of fields that distinguish an instance of this model from another, on + a best-effort basis. + - Use things like names, labels, or other stable identifying features. + - NO numeric database IDs! + - Any foreign keys should be hydrated and the identity-hash of the foreign entity used as part of the hash. + - There's a [[hydrated-hash]] helper for this with several example uses." + (:require [potemkin.types :as p.types] + [toucan.hydrate :refer [hydrate]])) + +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | Identity Hashes | +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; Generated entity_id values have lately been added to most exported models, but they only get populated on newly +;;; created entities. Since we can't rely on entity_id being present, we need a content-based definition of identity for +;;; all exported models. +(p.types/defprotocol+ IdentityHashable + (identity-hash-fields + [entity] + "You probably want to call [[metabase.models.serialization.hash/identity-hash]] instead of calling this directly. + + Content-based identity for the entities we export in serialization. This should return a sequence of functions to + apply to an entity and then hash. For example, Metric's [[identity-hash-fields]] is `[:name :table]`. These + functions are mapped over each entity and [[clojure.core/hash]] called on the result. This gives a portable hash + value across JVMs, Clojure versions, and platforms. + + NOTE: No numeric database IDs! For any foreign key, use [[hydrated-hash]] to hydrate the foreign entity and include + its [[identity-hash]] as part of this hash. This is a portable way of capturing the foreign relationship.")) + +(defn raw-hash + "Hashes a Clojure value into an 8-character hex string, which is used as the identity hash. + Don't call this outside a test, use [[identity-hash]] instead." + [target] + (format "%08x" (hash target))) + +(defn identity-hash + "Given a modeled entity, return its identity hash for use in serialization. The hash is an 8-character hex string. + The hash is based on a set of fields on the entity, defined by its implementation of [[identity-hash-fields]]. + These hashes are intended to be a decently robust fallback for older entities whose `entity_id` fields are not + populated." + [entity] + (-> (for [f (identity-hash-fields entity)] + (f entity)) + raw-hash)) + +(defn hydrated-hash + "Many entities reference other entities. Using the autoincrementing ID is not portable, so we use the identity hash + of the referenced entity. This is a helper for writing [[identity-hash-fields]]." + [hydration-key] + (fn [entity] + (-> entity + (hydrate hydration-key) + (get hydration-key) + identity-hash))) diff --git a/src/metabase/models/setting.clj b/src/metabase/models/setting.clj index 3765dbdaf999c7426436fd7edf92cc6ba088a44f..b0d19c53736c231f2006351321872c3016a69ed6 100644 --- a/src/metabase/models/setting.clj +++ b/src/metabase/models/setting.clj @@ -81,6 +81,7 @@ [environ.core :as env] [medley.core :as m] [metabase.api.common :as api] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.setting.cache :as setting.cache] [metabase.plugins.classloader :as classloader] [metabase.util :as u] @@ -139,7 +140,10 @@ models/IModel (merge models/IModelDefaults {:types (constantly {:value :encrypted-text}) - :primary-key (constantly :key)})) + :primary-key (constantly :key)}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:key])}) (declare get-value-of-type) diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj index 62b46190c0a4968166169fe2cfb50fcb65fc6e58..7ad9cd2ef6e60b50a7abcb5e23eb1612b55c399b 100644 --- a/src/metabase/models/table.clj +++ b/src/metabase/models/table.clj @@ -11,6 +11,7 @@ [metabase.models.metric :refer [Metric retrieve-metrics]] [metabase.models.permissions :as perms :refer [Permissions]] [metabase.models.segment :refer [retrieve-segments Segment]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.util :as u] [toucan.db :as db] [toucan.models :as models])) @@ -75,7 +76,10 @@ (merge mi/IObjectPermissionsDefaults {:can-read? (partial mi/current-user-has-full-permissions? :read) :can-write? (partial mi/current-user-has-full-permissions? :write) - :perms-objects-set perms-objects-set})) + :perms-objects-set perms-objects-set}) + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:schema :name (serdes.hash/hydrated-hash :db)])}) ;;; ------------------------------------------------ Field ordering ------------------------------------------------- diff --git a/src/metabase/models/timeline.clj b/src/metabase/models/timeline.clj index e0e9232cc6f7abfcf1d459ea530791286417cc7c..cd6398ee2002fec0db9cc45c8a06d3b9c67a7543 100644 --- a/src/metabase/models/timeline.clj +++ b/src/metabase/models/timeline.clj @@ -2,6 +2,7 @@ (:require [metabase.models.collection :as collection] [metabase.models.interface :as mi] [metabase.models.permissions :as perms] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.timeline-event :as timeline-event] [metabase.util :as u] [schema.core :as s] @@ -55,4 +56,7 @@ :entity_id true})}) mi/IObjectPermissions - perms/IObjectPermissionsForParentCollection) + perms/IObjectPermissionsForParentCollection + + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:name (serdes.hash/hydrated-hash :collection)])}) diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index 87454288b80d27b8a904acdc4185c09852fd14bf..4ca349a6a33f35366640b0bc8a3cebc4b64f5f21 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -7,6 +7,7 @@ [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] [metabase.models.permissions-group-membership :as perms-group-membership :refer [PermissionsGroupMembership]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.session :refer [Session]] [metabase.plugins.classloader :as classloader] [metabase.public-settings :as public-settings] @@ -140,7 +141,9 @@ :pre-update pre-update :post-select post-select :types (constantly {:login_attributes :json-no-keywordization - :settings :encrypted-json})})) + :settings :encrypted-json})}) + serdes.hash/IdentityHashable + {:identity-hash-fields (constantly [:email])}) (defn group-ids "Fetch set of IDs of PermissionsGroup a User belongs to." diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj index 6bd68d3484a79203b08e22f817fa36d31e643b6c..f5a61e90351b815f481acf2cff329196c5211a62 100644 --- a/test/metabase/models/card_test.clj +++ b/test/metabase/models/card_test.clj @@ -3,6 +3,7 @@ [clojure.test :refer :all] [metabase.models :refer [Card Collection Dashboard DashboardCard]] [metabase.models.card :as card] + [metabase.models.serialization.hash :as serdes.hash] [metabase.query-processor :as qp] [metabase.test :as mt] [metabase.test.util :as tu] @@ -292,7 +293,6 @@ #"Invalid Field Filter: Field \d+ \"VENUES\"\.\"NAME\" belongs to Database \d+ \"test-data\", but the query is against Database \d+ \"sample-dataset\"" (db/update! Card card-id bad-card-data)))))))) - ;;; ------------------------------------------ Parameters tests ------------------------------------------ (deftest validate-parameters-test @@ -360,3 +360,11 @@ :card_id 1, :target [:dimension [:field 1 nil]]}] (db/select-one-field :parameter_mappings Card :id card-id)))))) + +(deftest identity-hash-test + (testing "Card hashes are composed of the name and the collection's hash" + (mt/with-temp* [Collection [coll {:name "field-db" :location "/"}] + Card [card {:name "the card" :collection_id (:id coll)}]] + (is (= "ead6cc05" + (serdes.hash/raw-hash ["the card" (serdes.hash/identity-hash coll)]) + (serdes.hash/identity-hash card)))))) diff --git a/test/metabase/models/collection_test.clj b/test/metabase/models/collection_test.clj index 94df9c324c79af0c63d384348f8176796b99ff0e..18cb780c8c0fb7d212cf2729ac481f214091383d 100644 --- a/test/metabase/models/collection_test.clj +++ b/test/metabase/models/collection_test.clj @@ -8,6 +8,7 @@ [metabase.models :refer [Card Collection Dashboard NativeQuerySnippet Permissions PermissionsGroup Pulse User]] [metabase.models.collection :as collection] [metabase.models.permissions :as perms] + [metabase.models.serialization.hash :as serdes.hash] [metabase.test :as mt] [metabase.test.fixtures :as fixtures] [metabase.util :as u] @@ -1613,3 +1614,21 @@ {:id 5 :here #{:card}}]}] (clean (collection/collections->tree {:card #{1 5} :dataset #{3 4}} collections)))))) + +(deftest identity-hash-test + (testing "Collection hashes are composed of the name, namespace, and parent collection's hash" + (mt/with-temp* [Collection [c1 {:name "top level" :namespace "yolocorp" :location "/"}] + Collection [c2 {:name "nested" :namespace "yolocorp" :location (format "/%s/" (:id c1))}] + Collection [c3 {:name "grandchild" :namespace "yolocorp" :location (format "/%s/%s/" (:id c1) (:id c2))}]] + (let [c1-hash (serdes.hash/identity-hash c1) + c2-hash (serdes.hash/identity-hash c2)] + (is (= "37e57249" + (serdes.hash/raw-hash ["top level" :yolocorp "ROOT"]) + c1-hash) + "Top-level collections should use a parent hash of 'ROOT'") + (is (= "ce76f360" + (serdes.hash/raw-hash ["nested" :yolocorp c1-hash]) + c2-hash)) + (is (= "acb1ea3e" + (serdes.hash/raw-hash ["grandchild" :yolocorp c2-hash]) + (serdes.hash/identity-hash c3))))))) diff --git a/test/metabase/models/dashboard_card_test.clj b/test/metabase/models/dashboard_card_test.clj index c2ecf65c7375e8f2cbf4e3f389c3cb2172880ec4..68ba72d37bdfbd5187f752383a7338504b491f2a 100644 --- a/test/metabase/models/dashboard_card_test.clj +++ b/test/metabase/models/dashboard_card_test.clj @@ -3,10 +3,12 @@ [clojure.test :refer :all] [metabase.models.card :refer [Card]] [metabase.models.card-test :as card-test] + [metabase.models.collection :refer [Collection]] [metabase.models.dashboard :refer [Dashboard]] [metabase.models.dashboard-card :as dashboard-card :refer [DashboardCard]] [metabase.models.dashboard-card-series :refer [DashboardCardSeries]] [metabase.models.interface-test :as i.test] + [metabase.models.serialization.hash :as serdes.hash] [metabase.test :as mt] [metabase.util :as u] [toucan.db :as db])) @@ -258,3 +260,15 @@ (is (= [] ((i.test/type-fn :parameters-list :out) (json/generate-string [])))))) + +(deftest identity-hash-test + (testing "Dashboard card hashes are composed of the card hash, dashboard hash, and visualization settings" + (mt/with-temp* [Collection [c1 {:name "top level" :location "/"}] + Dashboard [dash {:name "my dashboard" :collection_id (:id c1)}] + Card [card {:name "some question" :collection_id (:id c1)}] + DashboardCard [dashcard {:card_id (:id card) + :dashboard_id (:id dash) + :visualization_settings {}}]] + (is (= "c926aed0" + (serdes.hash/raw-hash [(serdes.hash/identity-hash card) (serdes.hash/identity-hash dash) {}]) + (serdes.hash/identity-hash dashcard)))))) diff --git a/test/metabase/models/dashboard_test.clj b/test/metabase/models/dashboard_test.clj index 73495dede6d5c9601342d72f01078b8ecb51be5a..4a473f1894f1829113a48e413a0b367d1fab15ad 100644 --- a/test/metabase/models/dashboard_test.clj +++ b/test/metabase/models/dashboard_test.clj @@ -12,6 +12,7 @@ [metabase.models.permissions :as perms] [metabase.models.pulse :refer [Pulse]] [metabase.models.pulse-card :refer [PulseCard]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.table :refer [Table]] [metabase.models.user :as user] [metabase.test :as mt] @@ -307,3 +308,11 @@ :type :category :target expected}] (db/select-one-field :parameters Dashboard :id dashboard-id)))))))) + +(deftest identity-hash-test + (testing "Dashboard hashes are composed of the name and parent collection's hash" + (mt/with-temp* [Collection [c1 {:name "top level" :location "/"}] + Dashboard [dash {:name "my dashboard" :collection_id (:id c1)}]] + (is (= "38c0adf9" + (serdes.hash/raw-hash ["my dashboard" (serdes.hash/identity-hash c1)]) + (serdes.hash/identity-hash dash)))))) diff --git a/test/metabase/models/database_test.clj b/test/metabase/models/database_test.clj index 236e41ff6a502addc774a947ae90cd11c9ec773c..7cb2908aa97caedae551f4ea461be28db0369e45 100644 --- a/test/metabase/models/database_test.clj +++ b/test/metabase/models/database_test.clj @@ -9,6 +9,7 @@ [metabase.models.database :as database] [metabase.models.permissions :as perms] [metabase.models.secret :as secret :refer [Secret]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.user :as user] [metabase.server.middleware.session :as mw.session] [metabase.task :as task] @@ -242,3 +243,11 @@ (mt/with-temp Database [{db-id :id} {:engine (u/qualified-name ::test)}] (is (= ::test (db/select-one-field :engine Database :id db-id)))))) + +(deftest identity-hash-test + (testing "Database hashes are composed of the name and engine" + (mt/with-temp Database [db {:engine :mysql :name "hashmysql"}] + (is (= (Integer/toHexString (hash ["hashmysql" :mysql])) + (serdes.hash/identity-hash db))) + (is (= "b6f1a9e8" + (serdes.hash/identity-hash db)))))) diff --git a/test/metabase/models/dependency_test.clj b/test/metabase/models/dependency_test.clj index 1ab5f7a1bdf488b23114e740cc75c7868490b485..ec66612f1b75f6d499c5912349d4bfacd2ff2200 100644 --- a/test/metabase/models/dependency_test.clj +++ b/test/metabase/models/dependency_test.clj @@ -1,6 +1,11 @@ (ns metabase.models.dependency-test (:require [clojure.test :refer :all] + [metabase.models.collection :refer [Collection]] + [metabase.models.database :refer [Database]] [metabase.models.dependency :as dependency :refer [Dependency]] + [metabase.models.metric :refer [Metric]] + [metabase.models.serialization.hash :as serdes.hash] + [metabase.models.table :refer [Table]] [metabase.test :as mt] [metabase.test.fixtures :as fixtures] [toucan.db :as db] @@ -91,3 +96,20 @@ :dependent_on_model "test" :dependent_on_id 2}} (format-dependencies (db/select Dependency, :model "Mock", :model_id 1)))))))) + +(deftest identity-hash-test + (testing "Dependency hashes are composed of the two model names and hashes of the target entities" + (mt/with-temp* [Collection [coll {:name "some collection" :location "/"}] + Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}] + Metric [metric {:name "measured" :table_id (:id table)}] + Dependency [dep {:model "Collection" + :model_id (:id coll) + :dependent_on_model "Metric" + :dependent_on_id (:id metric) + :created_at :%now}]] + (is (= "cd893624" + ; Note the extra vector here - dependencies have one complex hash extractor that returns a list of results. + (serdes.hash/raw-hash [["Collection" (serdes.hash/identity-hash coll) + "Metric" (serdes.hash/identity-hash metric)]]) + (serdes.hash/identity-hash dep)))))) diff --git a/test/metabase/models/dimension_test.clj b/test/metabase/models/dimension_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..461e83c879be442584ff7d9e2f9cec92dd4eb138 --- /dev/null +++ b/test/metabase/models/dimension_test.clj @@ -0,0 +1,19 @@ +(ns metabase.models.dimension-test + (:require [clojure.test :refer :all] + [metabase.models.database :refer [Database]] + [metabase.models.dimension :refer [Dimension]] + [metabase.models.field :refer [Field]] + [metabase.models.serialization.hash :as serdes.hash] + [metabase.models.table :refer [Table]] + [metabase.test :as mt])) + +(deftest identity-hash-test + (testing "Dimension hashes are composed of the proper field hash, and the human-readable field hash" + (mt/with-temp* [Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}] + Field [field1 {:name "sku" :table_id (:id table)}] + Field [field2 {:name "human" :table_id (:id table)}] + Dimension [dim {:field_id (:id field1) :human_readable_field_id (:id field2)}]] + (is (= "d579f125" + (serdes.hash/raw-hash [(serdes.hash/identity-hash field1) (serdes.hash/identity-hash field2)]) + (serdes.hash/identity-hash dim)))))) diff --git a/test/metabase/models/field_test.clj b/test/metabase/models/field_test.clj index f5099d6d839e518e6d336dfadcd6ccdd7a1df3a4..e98a4d72ba11e3bc97725e53ad31da36bb61e9ff 100644 --- a/test/metabase/models/field_test.clj +++ b/test/metabase/models/field_test.clj @@ -1,7 +1,10 @@ (ns metabase.models.field-test "Tests for specific behavior related to the Field model." (:require [clojure.test :refer :all] + [metabase.models.database :refer [Database]] [metabase.models.field :refer [Field]] + [metabase.models.serialization.hash :as serdes.hash] + [metabase.models.table :refer [Table]] [metabase.test :as mt] [metabase.util :as u] [metabase.util.honeysql-extensions :as hx] @@ -42,3 +45,13 @@ column unknown-type)) (mt/with-temp Field [field {column unknown-type}] field)))))) + +(deftest identity-hash-test + (testing "Field hashes are composed of the name and the table's identity-hash" + (mt/with-temp* [Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}] + Field [field {:name "sku" :table_id (:id table)}]] + (let [table-hash (serdes.hash/identity-hash table)] + (is (= "dfd77225" + (serdes.hash/raw-hash ["sku" table-hash]) + (serdes.hash/identity-hash field))))))) diff --git a/test/metabase/models/field_values_test.clj b/test/metabase/models/field_values_test.clj index c032c456564c586681bf3ca6e467518c5d6f5638..d720d32a12036ac0b73354d3e62e962339382ec7 100644 --- a/test/metabase/models/field_values_test.clj +++ b/test/metabase/models/field_values_test.clj @@ -9,6 +9,7 @@ [metabase.models.dimension :refer [Dimension]] [metabase.models.field :refer [Field]] [metabase.models.field-values :as field-values :refer [FieldValues]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.table :refer [Table]] [metabase.sync :as sync] [metabase.test :as mt] @@ -227,3 +228,13 @@ :values [1 2 3 4] :human_readable_values []} (field-values)))))))))) + +(deftest identity-hash-test + (testing "Field hashes are composed of the name and the table's identity-hash" + (mt/with-temp* [Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}] + Field [field {:name "sku" :table_id (:id table)}] + FieldValues [fv {:field_id (:id field)}]] + (is (= "6f5bb4ba" + (serdes.hash/raw-hash [(serdes.hash/identity-hash field)]) + (serdes.hash/identity-hash fv)))))) diff --git a/test/metabase/models/metric_test.clj b/test/metabase/models/metric_test.clj index cd2c2ca89e25bed52d9557d64a7f3b8190ffc1b5..0ec6bde1556ff8ed3508483afade4b81e1e057a5 100644 --- a/test/metabase/models/metric_test.clj +++ b/test/metabase/models/metric_test.clj @@ -2,6 +2,7 @@ (:require [clojure.test :refer :all] [metabase.models.database :refer [Database]] [metabase.models.metric :as metric :refer [Metric]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.table :refer [Table]] [metabase.test :as mt] [metabase.util :as u] @@ -163,3 +164,12 @@ 12 {:definition {:aggregation nil :filter nil}})))) + +(deftest identity-hash-test + (testing "Metric hashes are composed of the metric name and table identity-hash" + (mt/with-temp* [Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}] + Metric [metric {:name "measurement" :table_id (:id table)}]] + (is (= "8fb4650a" + (serdes.hash/raw-hash ["measurement" (serdes.hash/identity-hash table)]) + (serdes.hash/identity-hash metric)))))) diff --git a/test/metabase/models/native_query_snippet_test.clj b/test/metabase/models/native_query_snippet_test.clj index 462c384a492ead9d56cd4a4c6cfb6cd3ccb80881..067a3832339bd84fabc7fbc3b7dd4a10bb7ff8dc 100644 --- a/test/metabase/models/native_query_snippet_test.clj +++ b/test/metabase/models/native_query_snippet_test.clj @@ -1,6 +1,7 @@ (ns metabase.models.native-query-snippet-test (:require [clojure.test :refer :all] [metabase.models :refer [Collection NativeQuerySnippet]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.test :as mt] [toucan.db :as db])) @@ -56,3 +57,11 @@ clojure.lang.ExceptionInfo #"A NativeQuerySnippet can only go in Collections in the :snippets namespace" (db/update! NativeQuerySnippet snippet-id :collection_id dest-collection-id))))))) + +(deftest identity-hash-test + (testing "Native query snippet hashes are composed of the name and the collection's hash" + (mt/with-temp* [Collection [coll {:name "field-db" :namespace :snippets :location "/"}] + NativeQuerySnippet [snippet {:name "my snippet" :collection_id (:id coll)}]] + (is (= "0e4562b1" + (serdes.hash/raw-hash ["my snippet" (serdes.hash/identity-hash coll)]) + (serdes.hash/identity-hash snippet)))))) diff --git a/test/metabase/models/pulse_card_test.clj b/test/metabase/models/pulse_card_test.clj index 2b5dc007bb5062127c36761ed1a6b0b8bfd9afa4..be4cf34481feec61d975dd5ca139fcac3e551d47 100644 --- a/test/metabase/models/pulse_card_test.clj +++ b/test/metabase/models/pulse_card_test.clj @@ -1,10 +1,13 @@ (ns metabase.models.pulse-card-test (:require [clojure.test :refer :all] [metabase.models.card :refer [Card]] + [metabase.models.collection :refer [Collection]] [metabase.models.dashboard :refer [Dashboard]] [metabase.models.dashboard-card :refer [DashboardCard]] [metabase.models.pulse :refer [Pulse]] [metabase.models.pulse-card :refer :all] + [metabase.models.serialization.hash :as serdes.hash] + [metabase.test :as mt] [toucan.util.test :as tt])) (deftest test-next-position-for @@ -21,3 +24,14 @@ :dashboard_card_id dashcard-id :position 2}]] (is (= 3 (next-position-for pulse-id)))))) + +(deftest identity-hash-test + (testing "Pulse card hashes are composed of the pulse's hash and the card's hash" + (mt/with-temp* [Collection [coll1 {:name "field-db" :location "/"}] + Collection [coll2 {:name "other collection" :location "/"}] + Card [card {:name "the card" :collection_id (:id coll1)}] + Pulse [pulse {:name "my pulse" :collection_id (:id coll2)}] + PulseCard [pulse-card {:card_id (:id card) :pulse_id (:id pulse)}]] + (is (= "cd532201" + (serdes.hash/raw-hash [(serdes.hash/identity-hash pulse) (serdes.hash/identity-hash card)]) + (serdes.hash/identity-hash pulse-card)))))) diff --git a/test/metabase/models/pulse_channel_test.clj b/test/metabase/models/pulse_channel_test.clj index 83258764001dbfa52db617942bc508a2d55e3ce2..80c3e5229adc19d6ccdaf0a4ce3f73af66179bf0 100644 --- a/test/metabase/models/pulse_channel_test.clj +++ b/test/metabase/models/pulse_channel_test.clj @@ -1,9 +1,11 @@ (ns metabase.models.pulse-channel-test (:require [clojure.test :refer :all] [medley.core :as m] + [metabase.models.collection :refer [Collection]] [metabase.models.pulse :refer [Pulse]] [metabase.models.pulse-channel :as pulse-channel :refer [PulseChannel]] [metabase.models.pulse-channel-recipient :refer [PulseChannelRecipient]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.user :refer [User]] [metabase.test :as mt] [metabase.util :as u] @@ -439,3 +441,14 @@ #"Wrong email address for User [\d,]+" (pulse-channel/validate-email-domains {:recipients [{:email "rasta@example.com" :id (mt/user->id :rasta)}]})))))) + +(deftest identity-hash-test + (testing "Pulse channel hashes are composed of the pulse's hash, the channel type, and the details and the collection hash" + (mt/with-temp* [Collection [coll {:name "field-db" :location "/"}] + Pulse [pulse {:name "my pulse" :collection_id (:id coll)}] + PulseChannel [chan {:pulse_id (:id pulse) + :channel_type :email + :details {:emails ["cam@test.com"]}}]] + (is (= "ab5e6ff0" + (serdes.hash/raw-hash [(serdes.hash/identity-hash pulse) :email {:emails ["cam@test.com"]}]) + (serdes.hash/identity-hash chan)))))) diff --git a/test/metabase/models/pulse_test.clj b/test/metabase/models/pulse_test.clj index bfaa8f56c2d4c027dff83f992bf01659b7acb1ea..c7b20e4dc07619ce2da6866e08c08e8104ca3450 100644 --- a/test/metabase/models/pulse_test.clj +++ b/test/metabase/models/pulse_test.clj @@ -7,6 +7,7 @@ [metabase.models.interface :as mi] [metabase.models.permissions :as perms] [metabase.models.pulse :as pulse] + [metabase.models.serialization.hash :as serdes.hash] [metabase.test :as mt] [metabase.test.mock.util :refer [pulse-channel-defaults]] [metabase.util :as u] @@ -452,3 +453,11 @@ (binding [api/*current-user-id* (:creator_id subscription)] (is (not (mi/can-read? subscription))) (is (not (mi/can-write? subscription))))))) + +(deftest identity-hash-test + (testing "Pulse hashes are composed of the name and the collection hash" + (mt/with-temp* [Collection [coll {:name "field-db" :location "/"}] + Pulse [pulse {:name "my pulse" :collection_id (:id coll)}]] + (is (= "6432d0a9" + (serdes.hash/raw-hash ["my pulse" (serdes.hash/identity-hash coll)]) + (serdes.hash/identity-hash pulse)))))) diff --git a/test/metabase/models/segment_test.clj b/test/metabase/models/segment_test.clj index eb5baa274b1363cb4d548e9b23c6ea54565a805a..c75d00af3c76fdd699d8120a64c6c0dc5b3f3737 100644 --- a/test/metabase/models/segment_test.clj +++ b/test/metabase/models/segment_test.clj @@ -2,6 +2,7 @@ (:require [clojure.test :refer :all] [metabase.models.database :refer [Database]] [metabase.models.segment :as segment :refer [Segment]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.table :refer [Table]] [metabase.test :as mt] [metabase.util :as u] @@ -124,3 +125,12 @@ {:name "A" :description "Unchanged" :definition {:filter [:and [:> [:field 4 nil] "2014-10-19"]]}}))))) + +(deftest identity-hash-test + (testing "Segment hashes are composed of the segment name and table identity-hash" + (mt/with-temp* [Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}] + Segment [segment {:name "big customers" :table_id (:id table)}]] + (is (= "a40066a4" + (serdes.hash/raw-hash ["big customers" (serdes.hash/identity-hash table)]) + (serdes.hash/identity-hash segment)))))) diff --git a/test/metabase/models/setting_test.clj b/test/metabase/models/setting_test.clj index 6600902ac6f8faf916326ad8501eb49eaa06ec9a..62505521671284baff350a914465e53af8bec908 100644 --- a/test/metabase/models/setting_test.clj +++ b/test/metabase/models/setting_test.clj @@ -2,6 +2,7 @@ (:require [clojure.test :refer :all] [environ.core :as env] [medley.core :as m] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.setting :as setting :refer [defsetting Setting]] [metabase.models.setting.cache :as setting.cache] [metabase.test :as mt] @@ -906,3 +907,11 @@ (deferred-tru "test Setting") :user-local :allowed :database-local :allowed))))) + +(deftest identity-hash-test + (testing "Settings are hashed based on the key" + (mt/with-temporary-setting-values [test-setting-1 "123" + test-setting-2 "123"] + (is (= "5f7f150c" + (serdes.hash/raw-hash ["test-setting-1"]) + (serdes.hash/identity-hash (db/select-one Setting :key "test-setting-1"))))))) diff --git a/test/metabase/models/table_test.clj b/test/metabase/models/table_test.clj index 065b84e45fd0b1bcfaa17b27dd9cf6a0669228a3..2c97b1a410d5385a76eea5b4c8dcc88cfcd4c131 100644 --- a/test/metabase/models/table_test.clj +++ b/test/metabase/models/table_test.clj @@ -1,6 +1,8 @@ (ns metabase.models.table-test (:require [clojure.java.jdbc :as jdbc] [clojure.test :refer :all] + [metabase.models.database :refer [Database]] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.table :as table :refer [Table]] [metabase.sync :as sync] [metabase.test :as mt] @@ -66,3 +68,12 @@ (mt/with-temp Table [{table-id :id} {:schema schema-name}] (is (= schema-name (db/select-one-field :schema Table :id table-id)))))))) + +(deftest identity-hash-test + (testing "Table hashes are composed of the schema name, table name and the database's identity-hash" + (mt/with-temp* [Database [db {:name "field-db" :engine :h2}] + Table [table {:schema "PUBLIC" :name "widget" :db_id (:id db)}]] + (let [db-hash (serdes.hash/identity-hash db)] + (is (= "0395fe49" + (serdes.hash/raw-hash ["PUBLIC" "widget" db-hash]) + (serdes.hash/identity-hash table))))))) diff --git a/test/metabase/models/user_test.clj b/test/metabase/models/user_test.clj index 6bf5efd23440bb7e2618778a55d201f0e4056745..db9e76f41e0408096c050cb128c2af8418d7b15a 100644 --- a/test/metabase/models/user_test.clj +++ b/test/metabase/models/user_test.clj @@ -19,6 +19,7 @@ [metabase.models.collection-test :as collection-test] [metabase.models.permissions :as perms] [metabase.models.permissions-group :as perms-group] + [metabase.models.serialization.hash :as serdes.hash] [metabase.models.user :as user] [metabase.test :as mt] [metabase.test.data.users :as test.users] @@ -461,3 +462,10 @@ (is (db/update! User user-id :is_active false))) (testing "subscription should no longer exist" (is (not (subscription-exists?)))))))) + +(deftest identity-hash-test + (testing "User hashes are based on the email address" + (mt/with-temp User [user {:email "fred@flintston.es"}] + (is (= "e8d63472" + (serdes.hash/raw-hash ["fred@flintston.es"]) + (serdes.hash/identity-hash user))))))