diff --git a/resources/migrations/001_update_migrations.yaml b/resources/migrations/001_update_migrations.yaml index bfcbdc22629e1207a4d80f19e21f02e36e2dacf4..d557e4d23fdb0027b3fd5aa82f9f4af278dc66c2 100644 --- a/resources/migrations/001_update_migrations.yaml +++ b/resources/migrations/001_update_migrations.yaml @@ -6055,6 +6055,19 @@ databaseChangeLog: constraintName: idx_cache_config_unique_model columnNames: model, model_id + - changeSet: + id: v50.2024-03-21T17:41:00 + author: qnkhuat + comment: Add metabase_table.estimated_row_count + changes: + - addColumn: + tableName: metabase_table + columns: + - column: + name: estimated_row_count + type: bigint + remarks: The estimated row count + # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<< ######################################################################################################################## diff --git a/src/metabase/api/notify.clj b/src/metabase/api/notify.clj index bab649e22a37f9bc11cb206d0b931096e93fadbc..dc85d3b9bc8d734db50cd15e5c5ae31d30a2bd33 100644 --- a/src/metabase/api/notify.clj +++ b/src/metabase/api/notify.clj @@ -58,8 +58,8 @@ (let [driver (driver.u/database->driver database) {db-tables :tables} (driver/describe-database driver database)] (if-let [table (some (fn [table-in-db] - (when (= (dissoc table-in-db :description) - {:schema schema_name :name table_name}) + (when (and (= schema_name (:schema table-in-db)) + (= table_name (:name table-in-db))) table-in-db)) db-tables)] (let [created (sync-tables/create-or-reactivate-table! database table)] diff --git a/src/metabase/driver/postgres.clj b/src/metabase/driver/postgres.clj index fc673670f24d49047202b23ae3c971477f2dcf27..d37ab0c94d08b883e46858a22bb421958c417fa2 100644 --- a/src/metabase/driver/postgres.clj +++ b/src/metabase/driver/postgres.clj @@ -227,10 +227,12 @@ [:inline "m"] [:inline "MATERIALIZED VIEW"] :else nil] :type] - [:d.description :description]] - :from [[:pg_catalog.pg_namespace :n] - [:pg_catalog.pg_class :c]] - :left-join [[:pg_catalog.pg_description :d] [:and [:= :c.oid :d.objoid] [:= :d.objsubid 0] [:= :d.classoid [:raw "'pg_class'::regclass"]]]] + [:d.description :description] + [:stat.n_live_tup :estimated_row_count]] + :from [[:pg_catalog.pg_class :c]] + :join [[:pg_catalog.pg_namespace :n] [:= :c.relnamespace :n.oid]] + :left-join [[:pg_catalog.pg_description :d] [:and [:= :c.oid :d.objoid] [:= :d.objsubid 0] [:= :d.classoid [:raw "'pg_class'::regclass"]]] + [:pg_stat_user_tables :stat] [:and [:= :n.nspname :stat.schemaname] [:= :c.relname :stat.relname]]] :where [:and [:= :c.relnamespace :n.oid] ;; filter out system tables [(keyword "!~") :n.nspname "^pg_"] [:<> :n.nspname "information_schema"] diff --git a/src/metabase/driver/sql_jdbc.clj b/src/metabase/driver/sql_jdbc.clj index f8ed39c50245c057e96d0e814ea7bdff94179b63..036aa4b23dbc0ea1137b91e3c9fd6715b16b9b24 100644 --- a/src/metabase/driver/sql_jdbc.clj +++ b/src/metabase/driver/sql_jdbc.clj @@ -74,8 +74,8 @@ ((get-method driver/db-default-timezone :metabase.driver/driver) driver database))) (defmethod driver/execute-reducible-query :sql-jdbc - [driver query chans respond] - (sql-jdbc.execute/execute-reducible-query driver query chans respond)) + [driver query context respond] + (sql-jdbc.execute/execute-reducible-query driver query context respond)) (defmethod driver/notify-database-updated :sql-jdbc [_ database] diff --git a/src/metabase/sync/interface.clj b/src/metabase/sync/interface.clj index 28a7a64f69da2f1f364315f87ae6762dbafe3438..9dbf0b9d7121cb81ae0a758b5a70d47871ef0973 100644 --- a/src/metabase/sync/interface.clj +++ b/src/metabase/sync/interface.clj @@ -8,12 +8,15 @@ [metabase.util.malli.schema :as ms])) (mr/def ::DatabaseMetadataTable - [:map - [:name ::lib.schema.common/non-blank-string] - [:schema [:maybe ::lib.schema.common/non-blank-string]] - [:require-filter {:optional true} :boolean] + [:map {:closed true} + [:name ::lib.schema.common/non-blank-string] + [:schema [:maybe ::lib.schema.common/non-blank-string]] + ;; for databases that store an estimated row count in system tables (e.g: postgres) + [:estimated_row_count {:optional true} [:maybe :int]] + ;; for databases that support forcing query to include a filter (e.g: partitioned table on bigquery) + [:database_require_filter {:optional true} [:maybe :boolean]] ;; `:description` in this case should be a column/remark on the Table, if there is one. - [:description {:optional true} [:maybe :string]]]) + [:description {:optional true} [:maybe :string]]]) (def DatabaseMetadataTable "Schema for the expected output of `describe-database` for a Table." diff --git a/src/metabase/sync/sync_metadata.clj b/src/metabase/sync/sync_metadata.clj index 2ad2647af887cd82546310eeeded4004106244e9..dfa2529ec864b08f6eb70653f2faea43dba0a029 100644 --- a/src/metabase/sync/sync_metadata.clj +++ b/src/metabase/sync/sync_metadata.clj @@ -57,9 +57,9 @@ (sync-util/create-sync-step "sync-fks" sync-fks/sync-fks! sync-fks-summary) ;; Sync index info if the database supports it (sync-util/create-sync-step "sync-indexes" sync-indexes/maybe-sync-indexes! sync-indexes-summary) - ;; finally, sync the metadata metadata table if it exists. + ;; Sync the metadata metadata table if it exists. (sync-util/create-sync-step "sync-metabase-metadata" #(metabase-metadata/sync-metabase-metadata! % db-metadata)) - ;; Now sync the table privileges + ;; Now sync the table privileges if the database enables it (sync-util/create-sync-step "sync-table-privileges" sync-table-privileges/sync-table-privileges!)]) (mu/defn sync-db-metadata! diff --git a/src/metabase/sync/sync_metadata/tables.clj b/src/metabase/sync/sync_metadata/tables.clj index dc71e71d6bbe3021c75207a9e83754139b433edb..d99f514b227365635cabd74e6dddd51b605c4712 100644 --- a/src/metabase/sync/sync_metadata/tables.clj +++ b/src/metabase/sync/sync_metadata/tables.clj @@ -156,7 +156,7 @@ [table-metadata :- i/DatabaseMetadataTable metabase-table :- (ms/InstanceOf :model/Table)] (log/infof "Updating table metadata for %s" (sync-util/name-for-logging metabase-table)) - (let [to-update-keys [:description :database_require_filter] + (let [to-update-keys [:description :database_require_filter :estimated_row_count] old-table (select-keys metabase-table to-update-keys) new-table (select-keys (merge (zipmap to-update-keys (repeat nil)) @@ -193,10 +193,10 @@ (remove metabase-metadata/is-metabase-metadata-table?) (:tables db-metadata))) -(mu/defn ^:private db->our-metadata :- [:set i/DatabaseMetadataTable] +(mu/defn ^:private db->our-metadata :- [:set (ms/InstanceOf :model/Table)] "Return information about what Tables we have for this DB in the Metabase application DB." [database :- i/DatabaseInstance] - (set (t2/select [:model/Table :id :name :schema :description :database_require_filter] + (set (t2/select [:model/Table :id :name :schema :description :database_require_filter :estimated_row_count] :db_id (u/the-id database) :active true))) diff --git a/test/metabase/driver/postgres_test.clj b/test/metabase/driver/postgres_test.clj index 18d94eba10a46b057617a68456db20e25bf7a5c6..c810ff6b8e7e4a10a82681100352672a0bca6333 100644 --- a/test/metabase/driver/postgres_test.clj +++ b/test/metabase/driver/postgres_test.clj @@ -251,8 +251,23 @@ (mt/run-mbql-query people {:fields [$name $bird_id->birds.name]})))))))) -(defn- default-table-result [table-name] - {:name table-name, :schema "public", :description nil}) +(defn- default-table-result + ([table-name] + (default-table-result table-name {})) + ([table-name opts] + (merge + {:name table-name :schema "public" :description nil + ;; estimated-row-count is estimated, so the value can't be known for sure "during" test without + ;; VACUUM-ing. So for tests that don't concern the exact value of estimated-row-count, use schema instead + :estimated_row_count (mt/malli=? [:maybe :int])} + opts))) + +(defn- describe-database->tables + "Returns a seq of tables sorted by name from calling [[driver/describe-database]]." + [& args] + (->> (apply driver/describe-database args) + :tables + (sort-by :name))) (deftest materialized-views-test (mt/test-driver :postgres @@ -264,9 +279,9 @@ ["DROP MATERIALIZED VIEW IF EXISTS test_mview; CREATE MATERIALIZED VIEW test_mview AS SELECT 'Toucans are the coolest type of bird.' AS true_facts;"]) - (t2.with-temp/with-temp [Database database {:engine :postgres, :details (assoc details :dbname "materialized_views_test")}] - (is (= {:tables #{(default-table-result "test_mview")}} - (driver/describe-database :postgres database)))))))) + (mt/with-temp [:model/Database database {:engine :postgres, :details (assoc details :dbname "materialized_views_test")}] + (is (=? [(default-table-result "test_mview" {:estimated_row_count (mt/malli=? int?)})] + (describe-database->tables :postgres database)))))))) (deftest foreign-tables-test (mt/test-driver :postgres @@ -276,7 +291,8 @@ ;; You need to set `MB_POSTGRESQL_TEST_USER` in order for this to work apparently. ;; ;; make sure that the details include optional stuff like `:user`. Otherwise the test is going to FAIL. You can - ;; set it at run time from the REPL using [[mt/db-test-env-var!]]. + ;; set it at run time from the REPL using [[mt/db-test-env-var!]] + ;; (mt/db-test-env-var! :postgresql :user "postgres"). (is (mc/coerce [:map [:port :int] [:host :string] @@ -297,8 +313,9 @@ OPTIONS (user '" (:user details) "'); GRANT ALL ON public.local_table to PUBLIC;")]) (t2.with-temp/with-temp [Database database {:engine :postgres, :details (assoc details :dbname "fdw_test")}] - (is (= {:tables (set (map default-table-result ["foreign_table" "local_table"]))} - (driver/describe-database :postgres database)))))))) + (is (=? [(default-table-result "foreign_table") + (default-table-result "local_table" {:estimated_row_count (mt/malli=? int?)})] + (describe-database->tables :postgres database)))))))) (deftest recreated-views-test (mt/test-driver :postgres @@ -363,8 +380,8 @@ ;; now sync the DB (sync!) ;; all three of these tables should appear in the metadata (including, importantly, the "main" table) - (is (= {:tables (set (map default-table-result ["part_vals" "part_vals_0" "part_vals_1"]))} - (driver/describe-database :postgres database))))) + (is (=? (map default-table-result ["part_vals" "part_vals_0" "part_vals_1"]) + (describe-database->tables :postgres database))))) (log/warn (u/format-color 'yellow @@ -1378,7 +1395,9 @@ (is (= (into #{} (#'sql-jdbc.describe-database/jdbc-get-tables :postgres (.getMetaData conn) nil schema-pattern table-pattern ["TABLE" "PARTITIONED TABLE" "VIEW" "FOREIGN TABLE" "MATERIALIZED VIEW"])) - (into #{} (#'postgres/get-tables (mt/db) schemas tables)))))] + (into #{} (map #(dissoc % :estimated_row_count)) + (#'postgres/get-tables (mt/db) schemas tables)))))] + (doseq [stmt ["CREATE TABLE public.table (id INTEGER, type TEXT);" "CREATE UNIQUE INDEX idx_table_type ON public.table(type);" "CREATE TABLE public.partition_table (id INTEGER) PARTITION BY RANGE (id);" diff --git a/test/metabase/sync/sync_metadata/tables_test.clj b/test/metabase/sync/sync_metadata/tables_test.clj index 13db5903119d71e45c6999e6c82f806ec5a00c26..90bd81ecd9bc517fbfa44b1ce2a697ac0369aefc 100644 --- a/test/metabase/sync/sync_metadata/tables_test.clj +++ b/test/metabase/sync/sync_metadata/tables_test.clj @@ -3,7 +3,9 @@ (:require [clojure.java.jdbc :as jdbc] [clojure.test :refer :all] + [metabase.driver :as driver] [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn] + [metabase.driver.sql-jdbc.execute :as sql-jdbc.execute] [metabase.models :refer [Database Table]] [metabase.sync :as sync] [metabase.sync.sync-metadata.tables :as sync-tables] @@ -11,6 +13,7 @@ [metabase.test.data.interface :as tx] [metabase.test.data.sql :as sql.tx] [metabase.util :as u] + [next.jdbc :as next.jdbc] [toucan2.core :as t2])) (tx/defdataset db-with-some-cruft @@ -60,3 +63,18 @@ (is (=? {:active true :description "added comment"} (t2/select-one :model/Table (mt/id :user)))))))) + +(deftest sync-estimated-row-count-test + (mt/test-driver :postgres + (testing "Can sync row count" + (mt/dataset test-data + ;; row count is estimated so we VACUUM so the statistic table is updated before syncing + (sql-jdbc.execute/do-with-connection-with-options + driver/*driver* + (mt/db) + nil + (fn [conn] + (next.jdbc/execute! conn ["VACUUM;"]))) + (sync/sync-database! (mt/db) {:scan :schema}) + (is (= 100 + (t2/select-one-fn :estimated_row_count :model/Table (mt/id :venues))))))))