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