From 8f2cc20461f753e5ba77256811bb38c1df55b325 Mon Sep 17 00:00:00 2001 From: Noah Moss <32746338+noahmoss@users.noreply.github.com> Date: Mon, 6 Dec 2021 12:42:40 -0500 Subject: [PATCH] Backend for DB sync progress UX (#18952) --- resources/migrations/000_migrations.yaml | 67 ++++++++++++++++++++++ src/metabase/api/activity.clj | 2 +- src/metabase/api/database.clj | 3 +- src/metabase/api/search.clj | 10 ++-- src/metabase/api/setup.clj | 12 ++-- src/metabase/models/database.clj | 4 +- src/metabase/models/table.clj | 5 +- src/metabase/public_settings.clj | 13 +++++ src/metabase/search/config.clj | 3 +- src/metabase/sync/sync_metadata.clj | 6 +- src/metabase/sync/sync_metadata/fks.clj | 3 + src/metabase/sync/util.clj | 53 +++++++++++++---- test/metabase/api/activity_test.clj | 9 +-- test/metabase/api/database_test.clj | 72 +++++++++++++----------- test/metabase/api/field_test.clj | 8 +-- test/metabase/api/search_test.clj | 16 +++--- test/metabase/api/table_test.clj | 16 +++--- test/metabase/sync/util_test.clj | 58 +++++++++++++++++-- test/metabase/sync_test.clj | 16 +++--- 19 files changed, 281 insertions(+), 95 deletions(-) diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml index d7e37ec3c03..a6bf10be517 100644 --- a/resources/migrations/000_migrations.yaml +++ b/resources/migrations/000_migrations.yaml @@ -9795,6 +9795,73 @@ databaseChangeLog: constraints: nullable: false + - changeSet: + id: v42.00-066 + author: noahmoss + comment: >- + Added 0.42.0 - new columns for initial DB sync progress UX. Indicates whether a database has succesfully synced + at least one time. + changes: + - addColumn: + tableName: metabase_database + columns: + - column: + name: initial_sync_status + type: varchar(32) + remarks: "String indicating whether a database has completed its initial sync and is ready to use" + defaultValue: "complete" + constraints: + nullable: false + + - changeSet: + id: v42.00-067 + author: noahmoss + comment: >- + Added 0.42.0 - new columns for initial DB sync progress UX. Indicates whether a table has succesfully synced + at least one time. + changes: + - addColumn: + tableName: metabase_table + columns: + - column: + name: initial_sync_status + type: varchar(32) + remarks: "String indicating whether a table has completed its initial sync and is ready to use" + defaultValue: "complete" + constraints: + nullable: false + + - changeSet: + id: v42.00-068 + author: noahmoss + comment: >- + Added 0.42.0 - new columns for initial DB sync progress UX. Records the ID of the admin who added a database. + May be null for the sample dataset, or for databases added prior to 0.42.0. + changes: + - addColumn: + tableName: metabase_database + columns: + - column: + name: creator_id + type: int + remarks: "ID of the admin who added the database" + constraints: + nullable: true + + - changeSet: + id: v42.00-069 + author: noahmoss + comment: >- + Added 0.42.0 - adds FK constraint for creator_id column, containing the ID of the admin who added a database. + changes: + - addForeignKeyConstraint: + baseTableName: metabase_database + baseColumnNames: creator_id + referencedTableName: core_user + referencedColumnNames: id + constraintName: fk_database_creator_id + onDelete: SET NULL + # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<< ######################################################################################################################## diff --git a/src/metabase/api/activity.clj b/src/metabase/api/activity.clj index de9aafae95c..87820c47436 100644 --- a/src/metabase/api/activity.clj +++ b/src/metabase/api/activity.clj @@ -96,7 +96,7 @@ "card" [Card :id :name :collection_id :description :display :dataset_query :dataset] "dashboard" [Dashboard :id :name :collection_id :description] - "table" [Table :id :name :db_id :display_name]) + "table" [Table :id :name :db_id :display_name :initial_sync_status]) {:where [:in :id ids]}))) (by-id [models] (m/index-by :id models))] (into {} (map (fn [[model models]] diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj index e093e6f630d..84fe1611864 100644 --- a/src/metabase/api/database.clj +++ b/src/metabase/api/database.clj @@ -501,7 +501,8 @@ :details details-or-error :is_full_sync is-full-sync? :is_on_demand (boolean is_on_demand) - :cache_ttl cache_ttl} + :cache_ttl cache_ttl + :creator_id api/*current-user-id*} (sync.schedules/schedule-map->cron-strings (if (:let-user-control-scheduling details) (sync.schedules/scheduling schedules) diff --git a/src/metabase/api/search.clj b/src/metabase/api/search.clj index 9f2fd473d39..f83bb8cae50 100644 --- a/src/metabase/api/search.clj +++ b/src/metabase/api/search.clj @@ -89,7 +89,9 @@ :database_id :integer :table_schema :text :table_name :text - :table_description :text)) + :table_description :text + ;; returned for Database and Table + :initial_sync_status :text)) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | Shared Query Logic | @@ -317,7 +319,7 @@ {:select (:select base-query) :from [[(merge base-query - {:select [:id :schema :db_id :name :description :display_name :updated_at + {:select [:id :schema :db_id :name :description :display_name :updated_at :initial_sync_status [(hx/concat (hx/literal "/db/") :db_id (hx/literal "/schema/") @@ -341,7 +343,7 @@ columns-to-search (->> all-search-columns (filter (fn [[k v]] (= v :text))) (map first) - (remove #{:collection_authority_level :moderated_status})) + (remove #{:collection_authority_level :moderated_status :initial_sync_status})) case-clauses (as-> columns-to-search <> (map (fn [col] [:like (hsql/call :lower col) match]) <>) (interleave <> (repeat 0)) @@ -438,7 +440,7 @@ models :- (s/maybe models-schema) limit :- (s/maybe su/IntGreaterThanZero) offset :- (s/maybe su/IntGreaterThanOrEqualToZero)] - (cond-> {:search-string search-string + (cond-> {:search-string search-string :archived? (Boolean/parseBoolean archived-string) :current-user-perms @api/*current-user-permissions-set*} (some? table-db-id) (assoc :table-db-id table-db-id) diff --git a/src/metabase/api/setup.clj b/src/metabase/api/setup.clj index ab3078a6798..35656edd759 100644 --- a/src/metabase/api/setup.clj +++ b/src/metabase/api/setup.clj @@ -58,14 +58,14 @@ (defn- setup-create-database! "Create a new Database. Returns newly created Database." - [{:keys [name driver details schedules database]}] + [{:keys [name driver details schedules database creator-id]}] (when driver (when-not (some-> (u/ignore-exceptions (driver/the-driver driver)) driver/available?) (let [msg (tru "Cannot create Database: cannot find driver {0}." driver)] (throw (ex-info msg {:errors {:database {:engine msg}}, :status-code 400})))) (db/insert! Database (merge - {:name name, :engine driver, :details details} + {:name name, :engine driver, :details details, :creator_id creator-id} (u/select-non-nil-keys database #{:is_on_demand :is_full_sync :auto_run_queries}) (when schedules (sync.schedules/schedule-map->cron-strings schedules)))))) @@ -106,8 +106,12 @@ (db/transaction (let [user-info (setup-create-user! {:email email, :first-name first_name, :last-name last_name, :password password}) - db (setup-create-database! - {:name name, :driver engine, :details details, :schedules schedules, :database database})] + db (setup-create-database! {:name name + :driver engine + :details details + :schedules schedules + :database database + :creator-id (:user-id user-info)})] (setup-set-settings! request {:email email, :site-name site_name, :site-locale site_locale, :allow-tracking? allow_tracking}) diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj index d1a1d4c43ea..c84f859fc1b 100644 --- a/src/metabase/models/database.clj +++ b/src/metabase/models/database.clj @@ -176,7 +176,9 @@ :cache_field_values_schedule new-fieldvalues-schedule))))))) (defn- pre-insert [database] - (handle-secrets-changes database)) + (-> database + handle-secrets-changes + (assoc :initial_sync_status "incomplete"))) (defn- perms-objects-set [database _] #{(perms/data-perms-path (u/the-id database))}) diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj index 1bf0d418c4c..3b451dfd3a4 100644 --- a/src/metabase/models/table.clj +++ b/src/metabase/models/table.clj @@ -37,8 +37,9 @@ ;;; --------------------------------------------------- Lifecycle ---------------------------------------------------- (defn- pre-insert [table] - (let [defaults {:display_name (humanization/name->human-readable-name (:name table)) - :field_order (driver/default-field-order (-> table :db_id Database :engine))}] + (let [defaults {:display_name (humanization/name->human-readable-name (:name table)) + :field_order (driver/default-field-order (-> table :db_id Database :engine)) + :initial_sync_status "incomplete"}] (merge defaults table))) (defn- pre-delete [{:keys [db_id schema id]}] diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj index 3010e8ea852..415fd8c53b5 100644 --- a/src/metabase/public_settings.clj +++ b/src/metabase/public_settings.clj @@ -453,3 +453,16 @@ :type :json :setter :none :getter fetch-cloud-gateway-ips-fn) + +(defsetting enable-database-syncing-modal + (str (deferred-tru "Whether an introductory modal should be shown after the next database connection is added.") + " " + (deferred-tru "Defaults to false if any non-default database connections already exist for this instance.")) + :visibility :admin + :type :boolean + :getter (fn [] + (let [v (setting/get-boolean :enable-database-syncing-modal)] + (if (nil? v) + (not (db/exists? 'Database :is_sample false)) + ;; frontend should set this value to `true` after the modal has been shown once + v)))) diff --git a/src/metabase/search/config.clj b/src/metabase/search/config.clj index d73e989885f..ea9f237690b 100644 --- a/src/metabase/search/config.clj +++ b/src/metabase/search/config.clj @@ -145,7 +145,7 @@ (defmethod columns-for-model "database" [_] - [:id :name :description :updated_at]) + [:id :name :description :updated_at :initial_sync_status]) (defmethod columns-for-model "pulse" [_] @@ -171,6 +171,7 @@ :display_name :description :updated_at + :initial_sync_status [:id :table_id] [:db_id :database_id] [:schema :table_schema] diff --git a/src/metabase/sync/sync_metadata.clj b/src/metabase/sync/sync_metadata.clj index f612cd49cc5..97e5114e20e 100644 --- a/src/metabase/sync/sync_metadata.clj +++ b/src/metabase/sync/sync_metadata.clj @@ -13,6 +13,7 @@ [metabase.sync.sync-metadata.sync-timezone :as sync-tz] [metabase.sync.sync-metadata.tables :as sync-tables] [metabase.sync.util :as sync-util] + [metabase.util :as u] [metabase.util.i18n :refer [trs]] [schema.core :as s])) @@ -46,7 +47,10 @@ "Sync the metadata for a Metabase `database`. This makes sure child Table & Field objects are synchronized." [database :- i/DatabaseInstance] (sync-util/sync-operation :sync-metadata database (format "Sync metadata for %s" (sync-util/name-for-logging database)) - (sync-util/run-sync-operation "sync" database sync-steps))) + (u/prog1 (sync-util/run-sync-operation "sync" database sync-steps) + (if (some sync-util/abandon-sync? (map second (:steps <>))) + (sync-util/set-initial-database-sync-aborted! database) + (sync-util/set-initial-database-sync-complete! database))))) (s/defn sync-table-metadata! "Sync the metadata for an individual `table` -- make sure Fields and FKs are up-to-date." diff --git a/src/metabase/sync/sync_metadata/fks.clj b/src/metabase/sync/sync_metadata/fks.clj index bea6c74762d..fdb2a8ff020 100644 --- a/src/metabase/sync/sync_metadata/fks.clj +++ b/src/metabase/sync/sync_metadata/fks.clj @@ -77,6 +77,9 @@ [database :- i/DatabaseInstance] (reduce (fn [update-info table] (let [table-fk-info (sync-fks-for-table! database table)] + ;; Mark the table as done with its initial sync once this step is done even if it failed, because only + ;; sync-aborting errors should be surfaced to the UI (see [[sync-util/exception-classes-not-to-retry]]). + (sync-util/set-initial-table-sync-complete! table) (if (instance? Exception table-fk-info) (update update-info :total-failed inc) (merge-with + update-info table-fk-info)))) diff --git a/src/metabase/sync/util.clj b/src/metabase/sync/util.clj index b93a133be9e..f8d5406ffb8 100644 --- a/src/metabase/sync/util.clj +++ b/src/metabase/sync/util.clj @@ -10,6 +10,7 @@ [metabase.driver :as driver] [metabase.driver.util :as driver.u] [metabase.events :as events] + [metabase.models.database :refer [Database]] [metabase.models.table :refer [Table]] [metabase.models.task-history :refer [TaskHistory]] [metabase.query-processor.interface :as qpi] @@ -251,6 +252,32 @@ ~emoji-progress-fn-binding (fn [] (emoji-progress-bar (swap! finished-count# inc) total-count# log-every-n#))] ~@body)) +;;; +----------------------------------------------------------------------------------------------------------------+ +;;; | INITIAL SYNC STATUS | +;;; +----------------------------------------------------------------------------------------------------------------+ + +;; If this is the first sync of a database, we need to update the `initial_sync_status` field on individual tables +;; when they have finished syncing, as well as the corresponding field on the database itself when the entire sync +;; is complete (excluding analysis). This powers a UX that displays the progress of the initial sync to the admin who +;; added the database, and enables individual tables when they become usable for queries. + +(defn set-initial-table-sync-complete! + "Marks initial sync as complete for this table so that it becomes usable in the UI, if not already set" + [table] + (when (not= (:initial_sync_status table) "complete") + (db/update! Table (u/the-id table) :initial_sync_status "complete"))) + +(defn set-initial-database-sync-complete! + "Marks initial sync as complete for this database so that this is reflected in the UI, if not already set" + [database] + (when (not= (:initial_sync_status database) "complete") + (db/update! Database (u/the-id database) :initial_sync_status "complete"))) + +(defn set-initial-database-sync-aborted! + "Marks initial sync as aborted for this database so that an error can be displayed on the UI" + [database] + (when (not= (:initial_sync_status database) "complete") + (db/update! Database (u/the-id database) :initial_sync_status "aborted"))) ;;; +----------------------------------------------------------------------------------------------------------------+ ;;; | OTHER SYNC UTILITY FUNCTIONS | @@ -349,7 +376,7 @@ (comp str log-summary-fn))})) (s/defn run-step-with-metadata :- StepNameWithMetadata - "Runs `step` on `database returning metadata from the run" + "Runs `step` on `database` returning metadata from the run" [database :- i/DatabaseInstance {:keys [step-name sync-fn log-summary-fn] :as step} :- StepDefinition] (let [start-time (t/zoned-date-time) @@ -441,6 +468,16 @@ (catch Throwable e (log/warn e (trs "Error saving task history"))))) +(defn abandon-sync? + "Given the results of a sync step, returns true if a non-recoverable exception occurred" + [step-results] + (when (contains? step-results :throwable) + (let [caught-exception (:throwable step-results) + exception-classes (u/full-exception-chain caught-exception)] + (some true? (for [ex exception-classes + test-ex exception-classes-not-to-retry] + (= (.. ^Object ex getClass getName) (.. ^Class test-ex getName))))))) + (s/defn run-sync-operation "Run `sync-steps` and log a summary message" [operation :- s/Str @@ -451,17 +488,9 @@ result []] (let [[step-name r] (run-step-with-metadata database step-defn) new-result (conj result [step-name r])] - (if (contains? r :throwable) - (let [caught-exception (:throwable r) - exception-classes (u/full-exception-chain caught-exception) - abandon? (some true? (for [ex exception-classes - test-ex exception-classes-not-to-retry] - (= (.. ^Object ex getClass getName) (.. ^Class test-ex getName))))] - (cond abandon? new-result - (not (seq rest-defns)) new-result - :else (recur rest-defns new-result))) - (cond (not (seq rest-defns)) new-result - :else (recur rest-defns new-result))))) + (cond (abandon-sync? r) new-result + (not (seq rest-defns)) new-result + :else (recur rest-defns new-result)))) end-time (t/zoned-date-time) sync-metadata {:start-time start-time :end-time end-time diff --git a/test/metabase/api/activity_test.clj b/test/metabase/api/activity_test.clj index b7bb26f358b..f34b779aebc 100644 --- a/test/metabase/api/activity_test.clj +++ b/test/metabase/api/activity_test.clj @@ -122,10 +122,11 @@ (is (= [{:cnt 1, :model "table", :model_id (:id table1), - :model_object {:db_id (:db_id table1), - :id (:id table1), - :name (:name table1) - :display_name (:display_name table1)}, + :model_object {:db_id (:db_id table1), + :id (:id table1), + :name (:name table1) + :display_name (:display_name table1) + :initial_sync_status "incomplete"}, :user_id (mt/user->id :crowberto)} {:cnt 1 :user_id (mt/user->id :crowberto) diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj index 41654584da3..513fc174fb7 100644 --- a/test/metabase/api/database_test.clj +++ b/test/metabase/api/database_test.clj @@ -45,19 +45,21 @@ (defn- db-details "Return default column values for a database (either the test database, via `(mt/db)`, or optionally passed in)." ([] - (db-details (mt/db))) + (-> (db-details (mt/db)) + (assoc :initial_sync_status "complete"))) ([{driver :engine, :as db}] (merge (mt/object-defaults Database) (select-keys db [:created_at :id :details :updated_at :timezone :name]) - {:engine (u/qualified-name (:engine db)) - :features (map u/qualified-name (driver.u/features driver db))}))) + {:engine (u/qualified-name (:engine db)) + :features (map u/qualified-name (driver.u/features driver db)) + :initial_sync_status "complete"}))) (defn- table-details [table] (-> (merge (mt/obj->json->obj (mt/object-defaults Table)) (select-keys table [:active :created_at :db_id :description :display_name :entity_type - :id :name :rows :schema :updated_at :visibility_type])) + :id :name :rows :schema :updated_at :visibility_type :initial_sync_status])) (update :entity_type #(when % (str "entity/" (name %)))) (update :visibility_type #(when % (name %))))) @@ -149,7 +151,8 @@ :details (s/eq {:db "my_db"}) :updated_at java.time.temporal.Temporal :name su/NonBlankString - :features (s/eq (driver.u/features ::test-driver (mt/db)))}) + :features (s/eq (driver.u/features ::test-driver (mt/db))) + :creator_id (s/eq (mt/user->id :crowberto))}) (create-db-via-api!)))) (testing "can we set `is_full_sync` to `false` when we create the Database?" @@ -236,41 +239,42 @@ (deftest fetch-database-metadata-test (testing "GET /api/database/:id/metadata" (is (= (merge (dissoc (mt/object-defaults Database) :details) - (select-keys (mt/db) [:created_at :id :updated_at :timezone]) + (select-keys (mt/db) [:created_at :id :updated_at :timezone :initial_sync_status]) {:engine "h2" :name "test-data" :features (map u/qualified-name (driver.u/features :h2 (mt/db))) :tables [(merge (mt/obj->json->obj (mt/object-defaults Table)) (db/select-one [Table :created_at :updated_at] :id (mt/id :categories)) - {:schema "PUBLIC" - :name "CATEGORIES" - :display_name "Categories" - :entity_type "entity/GenericTable" - :fields [(merge - (field-details (Field (mt/id :categories :id))) - {:table_id (mt/id :categories) - :semantic_type "type/PK" - :name "ID" - :display_name "ID" - :database_type "BIGINT" - :base_type "type/BigInteger" - :effective_type "type/BigInteger" - :visibility_type "normal" - :has_field_values "none" - :database_position 0}) - (merge - (field-details (Field (mt/id :categories :name))) - {:table_id (mt/id :categories) - :semantic_type "type/Name" - :name "NAME" - :display_name "Name" - :database_type "VARCHAR" - :base_type "type/Text" - :effective_type "type/Text" - :visibility_type "normal" - :has_field_values "list" - :database_position 1})] + {:schema "PUBLIC" + :name "CATEGORIES" + :display_name "Categories" + :entity_type "entity/GenericTable" + :initial_sync_status "complete" + :fields [(merge + (field-details (Field (mt/id :categories :id))) + {:table_id (mt/id :categories) + :semantic_type "type/PK" + :name "ID" + :display_name "ID" + :database_type "BIGINT" + :base_type "type/BigInteger" + :effective_type "type/BigInteger" + :visibility_type "normal" + :has_field_values "none" + :database_position 0}) + (merge + (field-details (Field (mt/id :categories :name))) + {:table_id (mt/id :categories) + :semantic_type "type/Name" + :name "NAME" + :display_name "Name" + :database_type "VARCHAR" + :base_type "type/Text" + :effective_type "type/Text" + :visibility_type "normal" + :has_field_values "list" + :database_position 1})] :segments [] :metrics [] :id (mt/id :categories) diff --git a/test/metabase/api/field_test.clj b/test/metabase/api/field_test.clj index 89795bebd1b..5040e35b316 100644 --- a/test/metabase/api/field_test.clj +++ b/test/metabase/api/field_test.clj @@ -20,8 +20,8 @@ (defn- db-details [] (merge - (select-keys (mt/db) [:id :timezone]) - (dissoc (mt/object-defaults Database) :details) + (select-keys (mt/db) [:id :timezone :initial_sync_status]) + (dissoc (mt/object-defaults Database) :details :initial_sync_status) {:engine "h2" :name "test-data" :features (mapv u/qualified-name (driver.u/features :h2 (mt/db))) @@ -36,7 +36,7 @@ {:table_id (mt/id :users) :table (merge (mt/obj->json->obj (mt/object-defaults Table)) - (db/select-one [Table :created_at :updated_at] :id (mt/id :users)) + (db/select-one [Table :created_at :updated_at :initial_sync_status] :id (mt/id :users)) {:description nil :entity_type "entity/UserTable" :visibility_type nil @@ -527,7 +527,7 @@ :fk_target_field_id false} (mt/boolean-ids-and-timestamps (simple-field-details (Field field-id-2)))))) (mt/user-http-request :crowberto :put 200 (format "field/%d" field-id-2) {:semantic_type :type/FK - :fk_target_field_id field-id-1}) + :fk_target_field_id field-id-1}) (testing "after change" (is (= {:name "Field Test 2" :display_name "Field Test 2" diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj index 8e817043b47..a2afced08f2 100644 --- a/test/metabase/api/search_test.clj +++ b/test/metabase/api/search_test.clj @@ -34,7 +34,8 @@ :table_schema nil :table_name nil :table_description nil - :updated_at true}) + :updated_at true + :initial_sync_status nil}) (defn- table-search-results "Segments and Metrics come back with information about their Tables as of 0.33.0. The `model-defaults` for Segment and @@ -470,12 +471,13 @@ (defn- default-table-search-row [table-name] (merge default-search-row - {:name table-name - :table_name table-name - :table_id true - :archived nil - :model "table" - :database_id true})) + {:name table-name + :table_name table-name + :table_id true + :archived nil + :model "table" + :database_id true + :initial_sync_status "incomplete"})) (defmacro ^:private do-test-users {:style/indent 1} [[user-binding users] & body] `(doseq [user# ~users diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj index a348540ca24..c43870df487 100644 --- a/test/metabase/api/table_test.clj +++ b/test/metabase/api/table_test.clj @@ -32,7 +32,7 @@ (defn- db-details [] (merge - (select-keys (mt/db) [:id :created_at :updated_at :timezone]) + (select-keys (mt/db) [:id :created_at :updated_at :timezone :creator_id :initial_sync_status]) {:engine "h2" :name "test-data" :is_sample false @@ -108,7 +108,7 @@ (testing "GET /api/table/:id" (is (= (merge (dissoc (table-defaults) :segments :field_values :metrics) - (db/select-one [Table :created_at :updated_at] :id (mt/id :venues)) + (db/select-one [Table :created_at :updated_at :initial_sync_status] :id (mt/id :venues)) {:schema "PUBLIC" :name "VENUES" :display_name "Venues" @@ -140,7 +140,7 @@ (testing "Sensitive fields are included" (is (= (merge (query-metadata-defaults) - (db/select-one [Table :created_at :updated_at] :id (mt/id :users)) + (db/select-one [Table :created_at :updated_at :initial_sync_status] :id (mt/id :users)) {:schema "PUBLIC" :name "USERS" :display_name "Users" @@ -203,7 +203,7 @@ (testing "Sensitive fields should not be included" (is (= (merge (query-metadata-defaults) - (db/select-one [Table :created_at :updated_at] :id (mt/id :users)) + (db/select-one [Table :created_at :updated_at :initial_sync_status] :id (mt/id :users)) {:schema "PUBLIC" :name "USERS" :display_name "Users" @@ -276,7 +276,7 @@ (-> (table-defaults) (dissoc :segments :field_values :metrics :updated_at) (assoc-in [:db :details] (:details (mt/db)))) - (db/select-one [Table :id :schema :name :created_at] :id (u/the-id table)) + (db/select-one [Table :id :schema :name :created_at :initial_sync_status] :id (u/the-id table)) {:description "What a nice table!" :entity_type nil :visibility_type "hidden" @@ -362,7 +362,7 @@ :position 2 :table (merge (dissoc (table-defaults) :segments :field_values :metrics) - (db/select-one [Table :id :created_at :updated_at] + (db/select-one [Table :id :created_at :updated_at :initial_sync_status] :id (mt/id :checkins)) {:schema "PUBLIC" :name "CHECKINS" @@ -379,7 +379,7 @@ :semantic_type "type/PK" :table (merge (dissoc (table-defaults) :db :segments :field_values :metrics) - (db/select-one [Table :id :created_at :updated_at] + (db/select-one [Table :id :created_at :updated_at :initial_sync_status] :id (mt/id :users)) {:schema "PUBLIC" :name "USERS" @@ -395,7 +395,7 @@ (testing "GET /api/table/:id/query_metadata" (is (= (merge (query-metadata-defaults) - (db/select-one [Table :created_at :updated_at] :id (mt/id :categories)) + (db/select-one [Table :created_at :updated_at :initial_sync_status] :id (mt/id :categories)) {:schema "PUBLIC" :name "CATEGORIES" :display_name "Categories" diff --git a/test/metabase/sync/util_test.clj b/test/metabase/sync/util_test.clj index d3ecf4fd8ad..2ec0442b6c6 100644 --- a/test/metabase/sync/util_test.clj +++ b/test/metabase/sync/util_test.clj @@ -5,9 +5,11 @@ [java-time :as t] [metabase.driver :as driver] [metabase.models.database :as mdb :refer [Database]] + [metabase.models.table :refer [Table]] [metabase.models.task-history :refer [TaskHistory]] [metabase.sync :as sync] - [metabase.sync.util :as sync-util :refer :all] + [metabase.sync.sync-metadata :as sync-metadata] + [metabase.sync.util :as sync-util] [metabase.test :as mt] [metabase.test.util :as tu] [toucan.db :as db] @@ -107,11 +109,11 @@ (let [process-name (tu/random-name) step-1-name (tu/random-name) step-2-name (tu/random-name) - sync-steps [(create-sync-step step-1-name (fn [_] (Thread/sleep 10) {:foo "bar"})) - (create-sync-step step-2-name (fn [_] (Thread/sleep 10)))] + sync-steps [(sync-util/create-sync-step step-1-name (fn [_] (Thread/sleep 10) {:foo "bar"})) + (sync-util/create-sync-step step-2-name (fn [_] (Thread/sleep 10)))] mock-db (mdb/map->DatabaseInstance {:name "test", :id 1, :engine :h2}) [results] (:operation-results - (call-with-operation-info #(run-sync-operation process-name mock-db sync-steps)))] + (call-with-operation-info #(sync-util/run-sync-operation process-name mock-db sync-steps)))] (testing "valid operation metadata?" (is (= true (validate-times results)))) @@ -245,3 +247,51 @@ (let [[step-name result] (second (:steps actual))] (is (= "should-continue" step-name)) (is (= {:log-summary-fn nil} (dissoc result :start-time :end-time)))))))) + +(deftest initial-sync-status-test + (mt/dataset sample-dataset + (testing "If `initial-sync-status` on a DB is `incomplete`, it is marked as `complete` when sync-metadata has finished" + (let [_ (db/update! Database (:id (mt/db)) :initial_sync_status "incomplete") + db (Database (:id (mt/db)))] + (sync/sync-database! db) + (is (= "complete" (db/select-one-field :initial_sync_status Database :id (:id db)))))) + + (testing "If `initial-sync-status` on a DB is `complete`, it remains `complete` when sync is run again" + (let [_ (db/update! Database (:id (mt/db)) :initial_sync_status "complete") + db (Database (:id (mt/db)))] + (sync/sync-database! db) + (is (= "complete" (db/select-one-field :initial_sync_status Database :id (:id db)))))) + + (testing "If `initial-sync-status` on a table is `incomplete`, it is marked as `complete` after the sync-fks step + has finished" + (let [table-id (db/select-one-field :id Table :db_id (:id (mt/db))) + _ (db/update! Table table-id :initial_sync_status "incomplete") + table (Table table-id)] + (sync/sync-database! (mt/db)) + (is (= "complete" (db/select-one-field :initial_sync_status Table :id table-id))))) + + (testing "Database and table syncs are marked as complete even if the initial scan is :schema only" + (let [_ (db/update! Database (:id (mt/db)) :initial_sync_status "incomplete") + db (Database (:id (mt/db))) + table-id (db/select-one-field :id Table :db_id (:id (mt/db))) + _ (db/update! Table table-id :initial_sync_status "incomplete") + table (Table table-id)] + (sync/sync-database! db {:scan :schema}) + (is (= "complete" (db/select-one-field :initial_sync_status Database :id (:id db)))) + (is (= "complete" (db/select-one-field :initial_sync_status Table :id table-id))))) + + (testing "If a non-recoverable error occurs during sync, `initial-sync-status` on the database is set to `aborted`" + (let [_ (db/update! Database (:id (mt/db)) :initial_sync_status "incomplete") + db (Database (:id (mt/db)))] + (with-redefs [sync-metadata/sync-steps [(sync-util/create-sync-step + "fake-step" + (fn [_] (throw (java.net.ConnectException.))))]] + (sync/sync-database! db) + (is (= "aborted" (db/select-one-field :initial_sync_status Database :id (:id db))))))) + + (testing "If `initial-sync-status` is `aborted` for a database, it is set to `complete` the next time sync finishes + without error" + (let [_ (db/update! Database (:id (mt/db)) :initial_sync_status "complete") + db (Database (:id (mt/db)))] + (sync/sync-database! db) + (is (= "complete" (db/select-one-field :initial_sync_status Database :id (:id db)))))))) diff --git a/test/metabase/sync_test.clj b/test/metabase/sync_test.clj index dbedef3db56..efdab7f1ce4 100644 --- a/test/metabase/sync_test.clj +++ b/test/metabase/sync_test.clj @@ -190,17 +190,19 @@ (testing "`movie` Table" (is (= (merge (table-defaults) - {:schema "default" - :name "movie" - :display_name "Movie" - :fields [(field:movie-id) (field:movie-studio) (field:movie-title)]}) + {:schema "default" + :name "movie" + :display_name "Movie" + :initial_sync_status "complete" + :fields [(field:movie-id) (field:movie-studio) (field:movie-title)]}) movie))) (testing "`studio` Table" (is (= (merge (table-defaults) - {:name "studio" - :display_name "Studio" - :fields [(field:studio-name) (field:studio-studio)]}) + {:name "studio" + :display_name "Studio" + :initial_sync_status "complete" + :fields [(field:studio-name) (field:studio-studio)]}) studio))))) (testing "Returns results from sync-database step" (mt/with-temp Database [db {:engine ::sync-test}] -- GitLab