From d9301a17673ed13477a643426a68314cc6d176db Mon Sep 17 00:00:00 2001 From: Cam Saul <1455846+camsaul@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:26:56 -0700 Subject: [PATCH] Add missing models to `copy` command (#34412) --- .../models/entity_id_test.clj | 4 +- src/metabase/cmd/copy.clj | 198 +++++++++--------- src/metabase/util/jvm.clj | 4 +- test/metabase/cmd/copy/h2_test.clj | 2 +- test/metabase/cmd/copy_test.clj | 46 ++-- test/metabase/cmd/load_and_dump_test.clj | 2 +- test/metabase/models/revision_test.clj | 89 ++++---- 7 files changed, 177 insertions(+), 168 deletions(-) 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 84d78345586..c64aa5983eb 100644 --- a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj +++ b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj @@ -44,7 +44,6 @@ :model/CardBookmark :model/CollectionBookmark :model/DashboardBookmark - :metabase.models.collection.root/RootCollection :model/CollectionPermissionGraphRevision :model/DashboardCardSeries :model/LoginHistory @@ -66,7 +65,6 @@ :model/QueryCache :model/QueryExecution :model/Revision - :model/FakedCard :model/Secret :model/Session :model/TablePrivileges @@ -79,6 +77,8 @@ (deftest ^:parallel comprehensive-entity-id-test (doseq [model (->> (v2.seed-entity-ids/toucan-models) + (remove (fn [model] + (not= (namespace model) "model"))) (remove entities-not-exported) (remove entities-external-name))] (testing (format (str "Model %s should either: have the ::mi/entity-id property, or be explicitly listed as having " diff --git a/src/metabase/cmd/copy.clj b/src/metabase/cmd/copy.clj index 7bd8152db7f..c32b6e36a55 100644 --- a/src/metabase/cmd/copy.clj +++ b/src/metabase/cmd/copy.clj @@ -4,58 +4,17 @@ data from an application database to any empty application database for all combinations of supported application database types." (:require + [clojure.java.classpath :as classpath] [clojure.java.jdbc :as jdbc] + [clojure.string :as str] + [clojure.tools.namespace.find :as ns.find] [honey.sql :as sql] + [metabase.config :as config] [metabase.db.connection :as mdb.connection] #_{:clj-kondo/ignore [:deprecated-namespace]} [metabase.db.data-migrations :refer [DataMigrations]] [metabase.db.setup :as mdb.setup] - [metabase.models - :refer [Action - Activity - ApplicationPermissionsRevision - BookmarkOrdering - Card - CardBookmark - Collection - CollectionBookmark - CollectionPermissionGraphRevision - Dashboard - DashboardBookmark - DashboardCard - DashboardCardSeries - Database - Dimension - Field - FieldValues - HTTPAction - ImplicitAction - LoginHistory - Metric - MetricImportantField - ModerationReview - NativeQuerySnippet - ParameterCard - Permissions - PermissionsGroup - PermissionsGroupMembership - PermissionsRevision - PersistedInfo - Pulse - PulseCard - PulseChannel - PulseChannelRecipient - QueryAction - Revision - Secret - Segment - Session - Setting - Table - Timeline - TimelineEvent - User - ViewLog]] + [metabase.plugins.classloader :as classloader] [metabase.util :as u] [metabase.util.i18n :refer [trs]] [metabase.util.log :as log] @@ -89,54 +48,63 @@ (def entities "Entities in the order they should be serialized/deserialized. This is done so we make sure that we load load instances of entities before others that might depend on them, e.g. `Databases` before `Tables` before `Fields`." - [Database - User - Setting - Table - Field - FieldValues - Segment - Metric - MetricImportantField - ModerationReview - Revision - ViewLog - Session - Collection - CollectionPermissionGraphRevision - Dashboard - Card - CardBookmark - DashboardBookmark - CollectionBookmark - BookmarkOrdering - DashboardCard - DashboardCardSeries - Activity - Pulse - PulseCard - PulseChannel - PulseChannelRecipient - PermissionsGroup - PermissionsGroupMembership - Permissions - PermissionsRevision - PersistedInfo - ApplicationPermissionsRevision - Dimension - NativeQuerySnippet - LoginHistory - Timeline - TimelineEvent - Secret - ParameterCard - Action - ImplicitAction - HTTPAction - QueryAction + (concat + [:model/Database + :model/User + :model/Setting + :model/Table + :model/Field + :model/FieldValues + :model/Segment + :model/Metric + :model/MetricImportantField + :model/ModerationReview + :model/Revision + :model/ViewLog + :model/Session + :model/Collection + :model/CollectionPermissionGraphRevision + :model/Dashboard + :model/Card + :model/CardBookmark + :model/DashboardBookmark + :model/CollectionBookmark + :model/BookmarkOrdering + :model/DashboardCard + :model/DashboardCardSeries + :model/Activity + :model/Pulse + :model/PulseCard + :model/PulseChannel + :model/PulseChannelRecipient + :model/PermissionsGroup + :model/PermissionsGroupMembership + :model/Permissions + :model/PermissionsRevision + :model/PersistedInfo + :model/ApplicationPermissionsRevision + :model/Dimension + :model/NativeQuerySnippet + :model/LoginHistory + :model/Timeline + :model/TimelineEvent + :model/Secret + :model/ParameterCard + :model/Action + :model/ImplicitAction + :model/HTTPAction + :model/QueryAction + :model/DashboardTab + :model/ModelIndex + :model/ModelIndexValue + ;; 48+ + :model/TablePrivileges] + (when config/ee-available? + [:model/GroupTableAccessPolicy + :model/ConnectionImpersonation]) ;; migrate the list of finished DataMigrations as the very last thing (all models to copy over should be listed ;; above this line) - DataMigrations]) + [DataMigrations])) (defn- objects->colums+values "Given a sequence of objects/rows fetched from the H2 DB, return a the `columns` that should be used in the `INSERT` @@ -346,7 +314,27 @@ (def ^:private entities-without-autoinc-ids "Entities that do NOT use an auto incrementing ID column." - #{Setting Session DataMigrations ImplicitAction HTTPAction QueryAction}) + #{:model/Setting + :model/Session + :model/DataMigrations + :model/ImplicitAction + :model/HTTPAction + :model/QueryAction + :model/ModelIndexValue + :model/TablePrivileges}) + +(defmulti ^:private postgres-id-sequence-name + {:arglists '([model])} + keyword) + +(defmethod postgres-id-sequence-name :default + [model] + (str (name (t2/table-name model)) "_id_seq")) + +;;; we changed the table name to `sandboxes` but never updated the underlying ID sequences or constraint names. +(defmethod postgres-id-sequence-name :model/GroupTableAccessPolicy + [_model] + "group_table_access_policy_id_seq") (defmulti ^:private update-sequence-values! {:arglists '([db-type data-source])} @@ -357,21 +345,26 @@ ;; Update the sequence nextvals. (defmethod update-sequence-values! :postgres - [_ data-source] + [_db-type data-source] #_{:clj-kondo/ignore [:discouraged-var]} (jdbc/with-db-transaction [target-db-conn {:datasource data-source}] (step (trs "Setting Postgres sequence ids to proper values...") - (doseq [e entities - :when (not (contains? entities-without-autoinc-ids e)) - :let [table-name (name (t2/table-name e)) - seq-name (str table-name "_id_seq") + (doseq [model entities + :when (not (contains? entities-without-autoinc-ids model)) + :let [table-name (name (t2/table-name model)) + seq-name (postgres-id-sequence-name model) sql (format "SELECT setval('%s', COALESCE((SELECT MAX(id) FROM %s), 1), true) as val" seq-name (name table-name))]] - (jdbc/db-query-with-resultset target-db-conn [sql] :val))))) + (try + (jdbc/db-query-with-resultset target-db-conn [sql] :val) + (catch Throwable e + (throw (ex-info (format "Error updating sequence values for %s: %s" model (ex-message e)) + {:model model} + e)))))))) (defmethod update-sequence-values! :h2 - [_ data-source] + [_db-type data-source] #_{:clj-kondo/ignore [:discouraged-var]} (jdbc/with-db-transaction [target-db-conn {:datasource data-source}] (step (trs "Setting H2 sequence ids to proper values...") @@ -389,6 +382,11 @@ source-data-source :- javax.sql.DataSource target-db-type :- (s/enum :h2 :postgres :mysql) target-data-source :- javax.sql.DataSource] + ;; make sure the entire system is loaded before running this test, to make sure we account for all the models. + (doseq [ns-symb (ns.find/find-namespaces (classpath/system-classpath)) + :when (and (str/starts-with? ns-symb "metabase") + (not (str/includes? ns-symb "test")))] + (classloader/require ns-symb)) ;; make sure the source database is up-do-date (step (trs "Set up {0} source database and run migrations..." (name source-db-type)) (mdb.setup/setup-db! source-db-type source-data-source true)) diff --git a/src/metabase/util/jvm.clj b/src/metabase/util/jvm.clj index e225a4155c9..db0a0f3fdbd 100644 --- a/src/metabase/util/jvm.clj +++ b/src/metabase/util/jvm.clj @@ -272,8 +272,8 @@ initialization."} metabase-namespace-symbols (vec (sort (for [ns-symb (ns.find/find-namespaces (classpath/system-classpath)) - :when (and (.startsWith (name ns-symb) "metabase.") - (not (.contains (name ns-symb) "test")))] + :when (and (str/starts-with? ns-symb "metabase.") + (not (str/includes? ns-symb "test")))] ns-symb)))) (defn deref-with-timeout diff --git a/test/metabase/cmd/copy/h2_test.clj b/test/metabase/cmd/copy/h2_test.clj index 8c3d7248762..ecff5f02709 100644 --- a/test/metabase/cmd/copy/h2_test.clj +++ b/test/metabase/cmd/copy/h2_test.clj @@ -4,7 +4,7 @@ [metabase.cmd.copy.h2 :as copy.h2] [metabase.db.data-source :as mdb.data-source])) -(deftest h2-data-source-test +(deftest ^:parallel h2-data-source-test (testing "works without file: schema" (is (= (mdb.data-source/raw-connection-string->DataSource "jdbc:h2:file:/path/to/metabase.db") (copy.h2/h2-data-source "/path/to/metabase.db")))) diff --git a/test/metabase/cmd/copy_test.clj b/test/metabase/cmd/copy_test.clj index 28473d445a8..9d7df9dd5cb 100644 --- a/test/metabase/cmd/copy_test.clj +++ b/test/metabase/cmd/copy_test.clj @@ -1,10 +1,11 @@ (ns metabase.cmd.copy-test (:require + [clojure.java.classpath :as classpath] + [clojure.string :as str] [clojure.test :refer :all] + [clojure.tools.namespace.find :as ns.find] [metabase.cmd.copy :as copy] - [metabase.db.util :as mdb.u] - [metabase.plugins.classloader :as classloader] - [metabase.util :as u])) + [metabase.plugins.classloader :as classloader])) (deftest ^:parallel sql-for-selecting-instances-from-source-db-test (is (= "SELECT * FROM metabase_field ORDER BY id ASC" @@ -22,19 +23,28 @@ [{:id 1, :engine "h2", :details "{:db \"metabase.db\"}"} {:id 2, :engine "postgres", :details "{:db \"metabase\"}"}]))))))) +(def ^:private models-to-exclude + "Models that should *not* be migrated in `load-from-h2`." + #{:model/TaskHistory + :model/Query + :model/QueryCache + :model/QueryExecution + :model/CardFavorite + :model/DashboardFavorite}) + +(defn- all-model-names [] + (into (sorted-set) + (comp (filter #(= (namespace %) "model")) + (remove models-to-exclude)) + (descendants :metabase/model))) + (deftest ^:paralell all-models-accounted-for-test - ;; This fetches the `metabase.cmd.load-from-h2/entities` and compares it all existing entities - (let [migrated-model-names (set (map :name copy/entities)) - ;; Models that should *not* be migrated in `load-from-h2`. - models-to-exclude #{"TaskHistory" "Query" "QueryCache" "QueryExecution" "CardFavorite" "DashboardFavorite"} - all-model-names (set (for [ns u/metabase-namespace-symbols - :when (or (re-find #"^metabase\.models\." (name ns)) - (= (name ns) "metabase.db.data-migrations")) - :when (not (re-find #"test" (name ns))) - [_ varr] (do (classloader/require ns) - (ns-interns ns)) - :let [{model-name :name, :as model} (var-get varr)] - :when (and (mdb.u/toucan-model? model) - (not (contains? models-to-exclude model-name)))] - model-name))] - (is (= all-model-names migrated-model-names)))) + ;; make sure the entire system is loaded before running this test, to make sure we account for all the models. + (doseq [ns-symb (ns.find/find-namespaces (classpath/system-classpath)) + :when (and (str/starts-with? ns-symb "metabase") + (not (str/includes? ns-symb "test")))] + (classloader/require ns-symb)) + (doseq [model (all-model-names) + :let [copy-models (set copy/entities)]] + (is (contains? copy-models model) + (format "%s should be added to %s, or to %s" model `copy/entities `models-to-exclude)))) diff --git a/test/metabase/cmd/load_and_dump_test.clj b/test/metabase/cmd/load_and_dump_test.clj index af2fee19d1f..6463b20b176 100644 --- a/test/metabase/cmd/load_and_dump_test.clj +++ b/test/metabase/cmd/load_and_dump_test.clj @@ -1,4 +1,4 @@ -(ns metabase.cmd.load-and-dump-test +(ns ^:mb/once metabase.cmd.load-and-dump-test (:require [clojure.java.io :as io] [clojure.test :refer :all] diff --git a/test/metabase/models/revision_test.clj b/test/metabase/models/revision_test.clj index 7acd672a8f0..a33dca66cf0 100644 --- a/test/metabase/models/revision_test.clj +++ b/test/metabase/models/revision_test.clj @@ -1,4 +1,4 @@ -(ns metabase.models.revision-test +(ns ^:mb/once metabase.models.revision-test (:require [clojure.test :refer :all] [metabase.models.card :refer [Card]] @@ -15,8 +15,8 @@ (def ^:private reverted-to (atom nil)) -(methodical/defmethod t2/table-name :model/FakedCard [_model] :report_card) -(derive :model/FakedCard :metabase/model) +(methodical/defmethod t2/table-name ::FakedCard [_model] :report_card) +(derive ::FakedCard :metabase/model) (defn- do-with-model-i18n-strs! [thunk] (with-redefs [revision.diff/model-str->i18n-str (fn [model-str] @@ -29,26 +29,26 @@ "FakeCard" "FakeCard"))] (thunk))) -(defmethod revision/serialize-instance :model/FakedCard +(defmethod revision/serialize-instance ::FakedCard [_model _id obj] (into {} (assoc obj :serialized true))) -(defmethod revision/revert-to-revision! :model/FakedCard +(defmethod revision/revert-to-revision! ::FakedCard [_model _id _user-id serialized-instance] (reset! reverted-to (dissoc serialized-instance :serialized))) -(defmethod revision/diff-map :model/FakedCard +(defmethod revision/diff-map ::FakedCard [_model o1 o2] {:o1 (when o1 (into {} o1)), :o2 (when o2 (into {} o2))}) -(defmethod revision/diff-strings :model/FakedCard +(defmethod revision/diff-strings ::FakedCard [_model o1 o2] (when o1 [(str "BEFORE=" (into {} o1) ",AFTER=" (into {} o2))])) (defn- push-fake-revision! [card-id & {:keys [message] :as object}] (revision/push-revision! - :entity :model/FakedCard + :entity ::FakedCard :id card-id :user-id (mt/user->id :rasta) :object (dissoc object :message) @@ -104,7 +104,7 @@ (testing "Test that a newly created Card doesn't have any revisions" (t2.with-temp/with-temp [Card {card-id :id}] (is (= [] - (revision/revisions :model/FakedCard card-id)))))) + (revision/revisions ::FakedCard card-id)))))) (deftest add-revision-test (testing "Test that we can add a revision" @@ -114,11 +114,11 @@ Revision {:model "FakedCard" :user_id (mt/user->id :rasta) - :object (mi/instance :model/FakedCard {:name "Tips Created by Day", :serialized true}) + :object (mi/instance 'FakedCard {:name "Tips Created by Day", :serialized true}) :is_reversion false :is_creation false :message "yay!"})] - (for [revision (revision/revisions :model/FakedCard card-id)] + (for [revision (revision/revisions ::FakedCard card-id)] (dissoc revision :timestamp :id :model_id))))))) (deftest sorting-test @@ -126,24 +126,25 @@ (t2.with-temp/with-temp [Card {card-id :id}] (push-fake-revision! card-id, :name "Tips Created by Day") (push-fake-revision! card-id, :name "Spots Created by Day") - (is (= [(mi/instance - Revision - {:model "FakedCard" - :user_id (mt/user->id :rasta) - :object (mi/instance :model/FakedCard {:name "Spots Created by Day", :serialized true}) - :is_reversion false - :is_creation false - :message nil}) - (mi/instance - Revision - {:model "FakedCard" - :user_id (mt/user->id :rasta) - :object (mi/instance :model/FakedCard {:name "Tips Created by Day", :serialized true}) - :is_reversion false - :is_creation false - :message nil})] - (->> (revision/revisions :model/FakedCard card-id) - (map #(dissoc % :timestamp :id :model_id)))))))) + (testing `revision/revisions + (is (= [(mi/instance + Revision + {:model "FakedCard" + :user_id (mt/user->id :rasta) + :object (mi/instance 'FakedCard {:name "Spots Created by Day", :serialized true}) + :is_reversion false + :is_creation false + :message nil}) + (mi/instance + Revision + {:model "FakedCard" + :user_id (mt/user->id :rasta) + :object (mi/instance 'FakedCard {:name "Tips Created by Day", :serialized true}) + :is_reversion false + :is_creation false + :message nil})] + (->> (revision/revisions ::FakedCard card-id) + (map #(dissoc % :timestamp :id :model_id))))))))) (deftest delete-old-revisions-test (testing "Check that old revisions get deleted" @@ -152,7 +153,7 @@ (dorun (doseq [i (range (inc revision/max-revisions))] (push-fake-revision! card-id, :name (format "Tips Created by Day %d" i)))) (is (= revision/max-revisions - (count (revision/revisions :model/FakedCard card-id))))))) + (count (revision/revisions ::FakedCard card-id))))))) (deftest do-not-record-if-object-is-not-changed-test (testing "Check that we don't record a revision if the object hasn't changed" @@ -161,15 +162,15 @@ (push-fake-revision! card-id, :name (format "Tips Created by Day %s" x)))] (testing "first revision should be recorded" (new-revision 1) - (is (= 1 (count (revision/revisions :model/FakedCard card-id))))) + (is (= 1 (count (revision/revisions ::FakedCard card-id))))) (testing "repeatedly push reivisions with the same object shouldn't create new revision" (dorun (repeatedly 5 #(new-revision 1))) - (is (= 1 (count (revision/revisions :model/FakedCard card-id))))) + (is (= 1 (count (revision/revisions ::FakedCard card-id))))) (testing "push a revision with different object should create new revision" (new-revision 2) - (is (= 2 (count (revision/revisions :model/FakedCard card-id)))))))) + (is (= 2 (count (revision/revisions ::FakedCard card-id)))))))) (testing "Check that we don't record revision on dashboard if it has a filter" (t2.with-temp/with-temp @@ -214,19 +215,19 @@ :o2 {:name "Modified Name", :serialized true}} :has_multiple_changes false :description "BEFORE={:name \"Initial Name\", :serialized true},AFTER={:name \"Modified Name\", :serialized true}."} - (let [revisions (revision/revisions :model/FakedCard card-id)] + (let [revisions (revision/revisions ::FakedCard card-id)] (assert (= 2 (count revisions))) - (-> (revision/add-revision-details :model/FakedCard (first revisions) (last revisions)) + (-> (revision/add-revision-details ::FakedCard (first revisions) (last revisions)) (dissoc :timestamp :id :model_id) mt/derecordize)))))) (testing "test that we return a description even when there is no change between revision" (is (= "created a revision with no change." - (str (:description (revision/add-revision-details :model/FakedCard {:name "Apple"} {:name "Apple"})))))) + (str (:description (revision/add-revision-details ::FakedCard {:name "Apple"} {:name "Apple"})))))) (testing "that we return a descrtiopn when there is no previous revision" (is (= "modified this." - (str (:description (revision/add-revision-details :model/FakedCard {:name "Apple"} nil))))))) + (str (:description (revision/add-revision-details ::FakedCard {:name "Apple"} nil))))))) (deftest revisions+details-test (testing "Check that revisions+details pulls in user info and adds description" @@ -242,7 +243,7 @@ :o2 {:name "Tips Created by Day", :serialized true}} :has_multiple_changes false :description "modified this."})] - (->> (revision/revisions+details :model/FakedCard card-id) + (->> (revision/revisions+details ::FakedCard card-id) (map #(dissoc % :timestamp :id :model_id)) (map #(update % :description str)))))))) @@ -272,7 +273,7 @@ :o2 {:name "Tips Created by Day", :serialized true}} :has_multiple_changes false :description "modified this."})] - (->> (revision/revisions+details :model/FakedCard card-id) + (->> (revision/revisions+details ::FakedCard card-id) (map #(dissoc % :timestamp :id :model_id)) (map #(update % :description str)))))))) @@ -282,8 +283,8 @@ (testing "Check that revert defers to revert-to-revision!" (t2.with-temp/with-temp [Card {card-id :id}] (push-fake-revision! card-id, :name "Tips Created by Day") - (let [[{revision-id :id}] (revision/revisions :model/FakedCard card-id)] - (revision/revert! :entity :model/FakedCard, :id card-id, :user-id (mt/user->id :rasta), :revision-id revision-id) + (let [[{revision-id :id}] (revision/revisions ::FakedCard card-id)] + (revision/revert! :entity ::FakedCard, :id card-id, :user-id (mt/user->id :rasta), :revision-id revision-id) (is (= {:name "Tips Created by Day"} @reverted-to)))))) @@ -304,8 +305,8 @@ (t2.with-temp/with-temp [Card {card-id :id}] (push-fake-revision! card-id, :name "Tips Created by Day") (push-fake-revision! card-id, :name "Spots Created by Day") - (let [[_ {old-revision-id :id}] (revision/revisions :model/FakedCard card-id)] - (revision/revert! :entity :model/FakedCard, :id card-id, :user-id (mt/user->id :rasta), :revision-id old-revision-id) + (let [[_ {old-revision-id :id}] (revision/revisions ::FakedCard card-id)] + (revision/revert! :entity ::FakedCard, :id card-id, :user-id (mt/user->id :rasta), :revision-id old-revision-id) (is (partial= [(mi/instance Revision @@ -331,7 +332,7 @@ :is_reversion false :is_creation false :message nil})] - (->> (revision/revisions :model/FakedCard card-id) + (->> (revision/revisions ::FakedCard card-id) (map #(dissoc % :timestamp :id :model_id))))))))) (deftest generic-models-revision-title+description-test -- GitLab