diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml index cf2875d2aa6233c11ec99f737e676f2dd91ef65c..db651b4e310dcc7f1f04377a87d5b7474347a203 100644 --- a/resources/migrations/000_migrations.yaml +++ b/resources/migrations/000_migrations.yaml @@ -6240,6 +6240,9 @@ databaseChangeLog: id: 165 author: sb comment: 'Added field_order to Table and database_position to Field' + validCheckSum: + - 8:474a357368a665d5e0701b9eb5f313f9 + - 8:8848644da9dd9e40924ae71ac4c7c370 changes: - addColumn: tableName: metabase_field @@ -6376,3 +6379,16 @@ databaseChangeLog: tableName: query_execution columnName: started_at newDataType: ${timestamp_type} + +# Remove `Table.rows`, which hasn't been used for years now. Older versions of +# Metabase used to store the row count in this column but we disabled it a long +# time ago for performance reasons. Now it's time to remove it entirely. + + - changeSet: + id: 169 + author: camsaul + comment: Added 0.36.0 + changes: + - dropColumn: + tableName: metabase_table + columnName: rows diff --git a/src/metabase/api/table.clj b/src/metabase/api/table.clj index 3bb08c04cfe34cda3b647b472c066cb9c8964fbf..9150e85f594b5cc7fc58122870502a167ced393d 100644 --- a/src/metabase/api/table.clj +++ b/src/metabase/api/table.clj @@ -37,13 +37,9 @@ (api/defendpoint GET "/" "Get all `Tables`." [] - (for [table (-> (db/select Table, :active true, {:order-by [[:name :asc]]}) - (hydrate :db)) - :when (mi/can-read? table)] - ;; if for some reason a Table doesn't have rows set then set it to 0 so UI doesn't barf. - ;; TODO - should that be part of `post-select` instead? - (update table :rows (fn [n] - (or n 0))))) + (as-> (db/select Table, :active true, {:order-by [[:name :asc]]}) tables + (hydrate tables :db) + (filterv mi/can-read? tables))) (api/defendpoint GET "/:id" "Get `Table` with ID." diff --git a/src/metabase/sync/analyze.clj b/src/metabase/sync/analyze.clj index 0a4d07b4f9416f23f52bc607faaf2f70118044d1..51b1bc53a24b94a25c5b6d9d1373f1b1fcc3471b 100644 --- a/src/metabase/sync/analyze.clj +++ b/src/metabase/sync/analyze.clj @@ -76,10 +76,8 @@ (update-last-analyzed! tables)) (s/defn analyze-table! - "Perform in-depth analysis for a TABLE." + "Perform in-depth analysis for a `table`." [table :- i/TableInstance] - ;; Table row count disabled for now because of performance issues - #_(table-row-count/update-row-count! table) (fingerprint/fingerprint-fields! table) (classify/classify-fields! table) (classify/classify-table! table) diff --git a/src/metabase/sync/analyze/table_row_count.clj b/src/metabase/sync/analyze/table_row_count.clj deleted file mode 100644 index 8efc35e9fb962b6808a351f630bc921cdee0aa00..0000000000000000000000000000000000000000 --- a/src/metabase/sync/analyze/table_row_count.clj +++ /dev/null @@ -1,26 +0,0 @@ -(ns metabase.sync.analyze.table-row-count - "Logic for updating a Table's row count by running appropriate MBQL queries." - (:require [clojure.tools.logging :as log] - [metabase.db.metadata-queries :as queries] - [metabase.models.table :refer [Table]] - [metabase.sync - [interface :as i] - [util :as sync-util]] - [metabase.util :as u] - [schema.core :as s] - [toucan.db :as db])) - -(s/defn ^:private table-row-count :- (s/maybe s/Int) - "Determine the count of rows in TABLE by running a simple structured MBQL query." - [table :- i/TableInstance] - (sync-util/with-error-handling (format "Unable to determine row count for %s" (sync-util/name-for-logging table)) - (queries/table-row-count table))) - -(s/defn update-row-count! - "Update the cached row count (`rows`) for a single TABLE." - [table :- i/TableInstance] - (sync-util/with-error-handling (format "Error setting table row count for %s" (sync-util/name-for-logging table)) - (when-let [row-count (table-row-count table)] - (log/debug (format "Set table row count for %s to %d" (sync-util/name-for-logging table) row-count)) - (db/update! Table (u/get-id table) - :rows row-count)))) diff --git a/test/expectations.clj b/test/expectations.clj index 84d31218b628b915281c4f5e6c5d147310062ee7..3e7f112aa92e3ca890d4bd194e2c6368a3fc4684 100644 --- a/test/expectations.clj +++ b/test/expectations.clj @@ -137,8 +137,8 @@ ;; expecting it. (when-not (env/env :drivers) (t/testing "Don't write any new tests using expect!" - (t/is (<= total-expect-forms 1778)) - (t/is (<= total-namespaces-using-expect 114)))))) + (t/is (<= total-expect-forms 1716)) + (t/is (<= total-namespaces-using-expect 108)))))) (defmacro ^:deprecated expect "Simple macro that simulates converts an Expectations-style `expect` form into a `clojure.test` `deftest` form." diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj index f3f19280bc58aa843c7a96e8f3bd145d3de3ba9b..bc1436629c406fbb51954dc0ba00ff319aef2f0f 100644 --- a/test/metabase/api/database_test.clj +++ b/test/metabase/api/database_test.clj @@ -59,7 +59,7 @@ (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_name :entity_type - :fields_hash :id :name :rows :schema :updated_at :visibility_type])) + :fields_hash :id :name :schema :updated_at :visibility_type])) (update :entity_type #(when % (str "entity/" (name %)))) (update :visibility_type #(when % (name %))))) @@ -244,7 +244,6 @@ :database_position 1})] :segments [] :metrics [] - :rows nil :id (mt/id :categories) :db_id (mt/id)})]}) (let [resp (mt/derecordize ((mt/user->client :rasta) :get 200 (format "database/%d/metadata" (mt/id))))] diff --git a/test/metabase/api/field_test.clj b/test/metabase/api/field_test.clj index 2d902159a023065c66a56667b58d5ade545b8703..a3d111efd83c7574c361eabc48a7c1766ff64063 100644 --- a/test/metabase/api/field_test.clj +++ b/test/metabase/api/field_test.clj @@ -45,7 +45,6 @@ :schema "PUBLIC" :name "USERS" :display_name "Users" - :rows nil :entity_name nil :active true :id (mt/id :users) diff --git a/test/metabase/api/permissions_test.clj b/test/metabase/api/permissions_test.clj index 00ba48e6549201e1c63a2aa7d90a06bdbaf8be92..a639afe69ab0c003244df386a46a3bce8621ee0f 100644 --- a/test/metabase/api/permissions_test.clj +++ b/test/metabase/api/permissions_test.clj @@ -1,93 +1,125 @@ (ns metabase.api.permissions-test "Tests for `/api/permissions` endpoints." (:require [clojure.test :refer :all] - [expectations :refer [expect]] + [metabase + [test :as mt] + [util :as u]] + [metabase.api.permissions :as permissions-api] [metabase.models [database :refer [Database]] [permissions :as perms] [permissions-group :as group :refer [PermissionsGroup]] [table :refer [Table]]] - [metabase.test - [data :as data] - [fixtures :as fixtures]] - [metabase.test.data.users :as test-users] - [metabase.util :as u] - [toucan.util.test :as tt])) + [metabase.test.fixtures :as fixtures] + [metabase.util.schema :as su] + [schema.core :as s])) +;; there are some issues where it doesn't look like the hydrate function for `member_count` is being added (?) +(comment permissions-api/keep-me) + +;; make sure test users are created first, otherwise we're possibly going to have some WEIRD results (use-fixtures :once (fixtures/initialize :test-users)) ;; GET /permissions/group ;; Should *not* include inactive users in the counts. ;; It should also *not* include the MetaBot group because MetaBot should *not* be enabled (defn- fetch-groups [] - (set ((test-users/user->client :crowberto) :get 200 "permissions/group"))) - -(expect - #{{:id (u/get-id (group/all-users)), :name "All Users", :member_count 3} - {:id (u/get-id (group/admin)), :name "Administrators", :member_count 1}} - ;; make sure test users are created first, otherwise we're possibly going to have some WEIRD results - (fetch-groups)) + (set ((mt/user->client :crowberto) :get 200 "permissions/group"))) -;; The endpoint should however return empty groups! -(tt/expect-with-temp [PermissionsGroup [group]] - #{{:id (u/get-id (group/all-users)), :name "All Users", :member_count 3} - {:id (u/get-id (group/admin)), :name "Administrators", :member_count 1} - (assoc (into {} group) :member_count 0)} - (fetch-groups)) +(deftest fetch-groups-test + (testing "GET /api/permissions/group" + (letfn [(check-default-groups-returned [id->group] + (testing "All Users Group should be returned" + (is (schema= {:id (s/eq (:id (group/all-users))) + :name (s/eq "All Users") + :member_count su/IntGreaterThanZero} + (get id->group (:id (group/all-users)))))) + (testing "Administrators Group should be returned" + (is (schema= {:id (s/eq (:id (group/admin))) + :name (s/eq "Administrators") + :member_count su/IntGreaterThanZero} + (get id->group (:id (group/admin)))))))] + (let [id->group (u/key-by :id (fetch-groups))] + (check-default-groups-returned id->group)) + (testing "should return empty groups" + (mt/with-temp PermissionsGroup [group] + (let [id->group (u/key-by :id (fetch-groups))] + (check-default-groups-returned id->group) + (testing "empty group should be returned" + (is (schema= {:id su/IntGreaterThanZero + :name su/NonBlankString + :member_count (s/eq 0)} + (get id->group (:id group))))))))))) -;; GET /permissions/group/:id -;; Should *not* include inactive users -(expect - #{{:first_name "Crowberto", :last_name "Corv", :email "crowberto@metabase.com", :user_id (test-users/user->id :crowberto), :membership_id true} - {:first_name "Lucky", :last_name "Pigeon", :email "lucky@metabase.com", :user_id (test-users/user->id :lucky), :membership_id true} - {:first_name "Rasta", :last_name "Toucan", :email "rasta@metabase.com", :user_id (test-users/user->id :rasta), :membership_id true}} - (set - (for [member (:members ((test-users/user->client :crowberto) :get 200 (str "permissions/group/" (u/get-id (group/all-users)))))] - (update member :membership_id some?)))) +(deftest fetch-group-test + (testing "GET /permissions/group/:id" + (let [{:keys [members]} ((mt/user->client :crowberto) :get 200 (format "permissions/group/%d" (:id (group/all-users)))) + id->member (u/key-by :user_id members)] + (is (schema= {:first_name (s/eq "Crowberto") + :last_name (s/eq "Corv") + :email (s/eq "crowberto@metabase.com") + :user_id (s/eq (mt/user->id :crowberto)) + :membership_id su/IntGreaterThanZero} + (get id->member (mt/user->id :crowberto)))) + (is (schema= {:first_name (s/eq "Lucky") + :last_name (s/eq "Pigeon") + :email (s/eq "lucky@metabase.com") + :user_id (s/eq (mt/user->id :lucky)) + :membership_id su/IntGreaterThanZero} + (get id->member (mt/user->id :lucky)))) + (is (schema= {:first_name (s/eq "Rasta") + :last_name (s/eq "Toucan") + :email (s/eq "rasta@metabase.com") + :user_id (s/eq (mt/user->id :rasta)) + :membership_id su/IntGreaterThanZero} + (get id->member (mt/user->id :rasta)))) + (testing "Should *not* include inactive users" + (is (= nil + (get id->member :trashbird))))))) +(deftest update-perms-graph-test + (testing "PUT /api/permissions/graph" + (testing "make sure we can update the perms graph from the API" + (mt/with-temp PermissionsGroup [group] + ((mt/user->client :crowberto) :put 200 "permissions/graph" + (assoc-in (perms/graph) + [:groups (u/get-id group) (mt/id) :schemas] + {"PUBLIC" {(mt/id :venues) :all}})) + (is (= {(mt/id :venues) :all} + (get-in (perms/graph) [:groups (u/get-id group) (mt/id) :schemas "PUBLIC"])))) -;; make sure we can update the perms graph from the API -(expect - {(data/id :venues) :all} - (tt/with-temp PermissionsGroup [group] - ((test-users/user->client :crowberto) :put 200 "permissions/graph" - (assoc-in (perms/graph) - [:groups (u/get-id group) (data/id) :schemas] - {"PUBLIC" {(data/id :venues) :all}})) - (get-in (perms/graph) [:groups (u/get-id group) (data/id) :schemas "PUBLIC"]))) + (testing "Table-specific perms" + (mt/with-temp PermissionsGroup [group] + ((mt/user->client :crowberto) :put 200 "permissions/graph" + (assoc-in (perms/graph) + [:groups (u/get-id group) (mt/id) :schemas] + {"PUBLIC" {(mt/id :venues) {:read :all, :query :segmented}}})) + (is (= {(mt/id :venues) {:read :all + :query :segmented}} + (get-in (perms/graph) [:groups (u/get-id group) (mt/id) :schemas "PUBLIC"])))))) -(expect - {(data/id :venues) {:read :all - :query :segmented}} - (tt/with-temp PermissionsGroup [group] - ((test-users/user->client :crowberto) :put 200 "permissions/graph" - (assoc-in (perms/graph) - [:groups (u/get-id group) (data/id) :schemas] - {"PUBLIC" {(data/id :venues) {:read :all, :query :segmented}}})) - (get-in (perms/graph) [:groups (u/get-id group) (data/id) :schemas "PUBLIC"]))) + (testing "permissions for new db" + (let [new-id (inc (mt/id))] + (mt/with-temp* [PermissionsGroup [group] + Database [{db-id :id}] + Table [_ {:db_id db-id}]] + ((mt/user->client :crowberto) :put 200 "permissions/graph" + (assoc-in (perms/graph) + [:groups (u/get-id group) db-id :schemas] + :all)) + (is (= :all + (get-in (perms/graph) [:groups (u/get-id group) db-id :schemas])))))) -;; permissions for new db -(expect - :all - (let [new-id (inc (data/id))] - (tt/with-temp* [PermissionsGroup [group] - Database [{db-id :id}] - Table [_ {:db_id db-id}]] - ((test-users/user->client :crowberto) :put 200 "permissions/graph" - (assoc-in (perms/graph) - [:groups (u/get-id group) db-id :schemas] - :all)) - (get-in (perms/graph) [:groups (u/get-id group) db-id :schemas])))) + (testing "permissions for new db with no tables" + (let [new-id (inc (mt/id))] + (mt/with-temp* [PermissionsGroup [group] + Database [{db-id :id}]] + ((mt/user->client :crowberto) :put 200 "permissions/graph" + (assoc-in (perms/graph) + [:groups (u/get-id group) db-id :schemas] + :all)) + (is (= :all + (get-in (perms/graph) [:groups (u/get-id group) db-id :schemas])))))))) -;; permissions for new db with no tables -(expect - :all - (let [new-id (inc (data/id))] - (tt/with-temp* [PermissionsGroup [group] - Database [{db-id :id}]] - ((test-users/user->client :crowberto) :put 200 "permissions/graph" - (assoc-in (perms/graph) - [:groups (u/get-id group) db-id :schemas] - :all)) - (get-in (perms/graph) [:groups (u/get-id group) db-id :schemas])))) +;; diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj index 1eb1a4083f520b0e8cc97d2d42c10cfb236787ce..9160c3f08e545671ec9410e9e539c7c1395848ad 100644 --- a/test/metabase/api/table_test.clj +++ b/test/metabase/api/table_test.clj @@ -4,7 +4,6 @@ [clojure [test :refer :all] [walk :as walk]] - [expectations :refer [expect]] [medley.core :as m] [metabase [http-client :as http] @@ -22,7 +21,6 @@ [metabase.test [data :as data] [util :as tu]] - [metabase.test.data.users :as test-users] [metabase.test.mock.util :as mutil] [metabase.timeseries-query-processor-test.util :as tqpt] [toucan.db :as db])) @@ -55,42 +53,27 @@ :auto_run_queries true})) (defn- table-defaults [] - {:description nil - :caveats nil - :points_of_interest nil - :show_in_getting_started false - :entity_type "entity/GenericTable" - :visibility_type nil - :db (db-details) - :entity_name nil - :active true - :db_id (mt/id) - :segments [] - :metrics [] - :field_order "database"}) - -(def ^:private field-defaults - {:description nil - :active true - :position 0 - :database_position 0 - :custom_position 0 - :target nil - :preview_display true - :visibility_type "normal" - :caveats nil - :points_of_interest nil - :special_type nil - :parent_id nil - :dimensions [] - :dimension_options [] - :has_field_values nil - :default_dimension_option nil - :settings nil}) + (merge + (mt/object-defaults Table) + {:db (db-details) + :entity_type "entity/GenericTable" + :field_order "database" + :metrics [] + :segments []})) + +(defn- field-defaults [] + (merge + (mt/object-defaults Field) + {:default_dimension_option nil + :dimension_options [] + :dimensions [] + :position 0 + :target nil + :visibility_type "normal"})) (defn- field-details [field] (merge - field-defaults + (field-defaults) (select-keys field [:created_at :fingerprint :fingerprint_version :fk_target_field_id :id :last_analyzed :updated_at]))) @@ -100,33 +83,29 @@ (dissoc :dimension_options :default_dimension_option))) -;; ## GET /api/table -;; These should come back in alphabetical order and include relevant metadata -(expect - #{{:name (mt/format-name "categories") - :display_name "Categories" - :rows 0 - :id (mt/id :categories) - :entity_type "entity/GenericTable"} - {:name (mt/format-name "checkins") - :display_name "Checkins" - :rows 0 - :id (mt/id :checkins) - :entity_type "entity/EventTable"} - {:name (mt/format-name "users") - :display_name "Users" - :rows 0 - :id (mt/id :users) - :entity_type "entity/UserTable"} - {:name (mt/format-name "venues") - :display_name "Venues" - :rows 0 - :id (mt/id :venues) - :entity_type "entity/GenericTable"}} - (->> ((mt/user->client :rasta) :get 200 "table") - (filter #(= (:db_id %) (mt/id))) ; prevent stray tables from affecting unit test results - (map #(select-keys % [:name :display_name :rows :id :entity_type])) - set)) +(deftest list-table-test + (testing "GET /api/table" + (testing "These should come back in alphabetical order and include relevant metadata" + (is (= #{{:name (mt/format-name "categories") + :display_name "Categories" + :id (mt/id :categories) + :entity_type "entity/GenericTable"} + {:name (mt/format-name "checkins") + :display_name "Checkins" + :id (mt/id :checkins) + :entity_type "entity/EventTable"} + {:name (mt/format-name "users") + :display_name "Users" + :id (mt/id :users) + :entity_type "entity/UserTable"} + {:name (mt/format-name "venues") + :display_name "Venues" + :id (mt/id :venues) + :entity_type "entity/GenericTable"}} + (->> ((mt/user->client :rasta) :get 200 "table") + (filter #(= (:db_id %) (mt/id))) ; prevent stray tables from affecting unit test results + (map #(select-keys % [:name :display_name :id :entity_type])) + set)))))) (deftest get-table-test (testing "GET /api/table/:id" @@ -136,7 +115,6 @@ {:schema "PUBLIC" :name "VENUES" :display_name "Venues" - :rows nil :pk_field (table/pk-field-id (Table (mt/id :venues))) :id (mt/id :venues) :db_id (mt/id)}) @@ -159,163 +137,128 @@ (-> (table-defaults) (assoc :dimension_options (default-dimension-options)))) -;; ## GET /api/table/:id/query_metadata -(expect - (merge - (query-metadata-defaults) - (db/select-one [Table :created_at :updated_at :fields_hash] :id (mt/id :categories)) - {:schema "PUBLIC" - :name "CATEGORIES" - :display_name "Categories" - :fields [(merge - (field-details (Field (mt/id :categories :id))) - {:table_id (mt/id :categories) - :special_type "type/PK" - :name "ID" - :display_name "ID" - :database_type "BIGINT" - :base_type "type/BigInteger" - :has_field_values "none"}) - (merge - (field-details (Field (mt/id :categories :name))) - {:table_id (mt/id :categories) - :special_type "type/Name" - :name "NAME" - :display_name "Name" - :database_type "VARCHAR" - :base_type "type/Text" - :dimension_options [] - :default_dimension_option nil - :has_field_values "list" - :database_position 1 - :position 1})] - :rows nil - :id (mt/id :categories)}) - ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :categories)))) - -;;; GET api/table/:id/query_metadata?include_sensitive_fields -;; Make sure that getting the User table *does* include info about the password field, but not actual values -;; themselves (deftest sensitive-fields-included-test - (testing "Sensitive fields are included" - (is (= (merge - (query-metadata-defaults) - (db/select-one [Table :created_at :updated_at :fields_hash] :id (mt/id :users)) - {:schema "PUBLIC" - :name "USERS" - :display_name "Users" - :entity_type "entity/UserTable" - :fields [(assoc (field-details (Field (mt/id :users :id))) - :special_type "type/PK" - :table_id (mt/id :users) - :name "ID" - :display_name "ID" - :database_type "BIGINT" - :base_type "type/BigInteger" - :visibility_type "normal" - :has_field_values "none") - (assoc (field-details (Field (mt/id :users :name))) - :special_type "type/Name" - :table_id (mt/id :users) - :name "NAME" - :display_name "Name" - :database_type "VARCHAR" - :base_type "type/Text" - :visibility_type "normal" - :dimension_options [] - :default_dimension_option nil - :has_field_values "list" - :position 1 - :database_position 1) - (assoc (field-details (Field (mt/id :users :last_login))) - :table_id (mt/id :users) - :name "LAST_LOGIN" - :display_name "Last Login" - :database_type "TIMESTAMP" - :base_type "type/DateTime" - :visibility_type "normal" - :dimension_options (var-get #'table-api/datetime-dimension-indexes) - :default_dimension_option (var-get #'table-api/date-default-index) - :has_field_values "none" - :position 2 - :database_position 2) - (assoc (field-details (Field :table_id (mt/id :users), :name "PASSWORD")) - :special_type "type/Category" - :table_id (mt/id :users) - :name "PASSWORD" - :display_name "Password" - :database_type "VARCHAR" - :base_type "type/Text" - :visibility_type "sensitive" - :has_field_values "list" - :position 3 - :database_position 3)] - :rows nil - :id (mt/id :users)}) - ((test-users/user->client :rasta) :get 200 (format "table/%d/query_metadata?include_sensitive_fields=true" (mt/id :users))))))) - -;;; GET api/table/:id/query_metadata -;;; Make sure that getting the User table does *not* include password info + (testing "GET api/table/:id/query_metadata?include_sensitive_fields" + (testing "Sensitive fields are included" + (is (= (merge + (query-metadata-defaults) + (db/select-one [Table :created_at :updated_at :fields_hash] :id (mt/id :users)) + {:schema "PUBLIC" + :name "USERS" + :display_name "Users" + :entity_type "entity/UserTable" + :fields [(assoc (field-details (Field (mt/id :users :id))) + :special_type "type/PK" + :table_id (mt/id :users) + :name "ID" + :display_name "ID" + :database_type "BIGINT" + :base_type "type/BigInteger" + :visibility_type "normal" + :has_field_values "none") + (assoc (field-details (Field (mt/id :users :name))) + :special_type "type/Name" + :table_id (mt/id :users) + :name "NAME" + :display_name "Name" + :database_type "VARCHAR" + :base_type "type/Text" + :visibility_type "normal" + :dimension_options [] + :default_dimension_option nil + :has_field_values "list" + :position 1 + :database_position 1) + (assoc (field-details (Field (mt/id :users :last_login))) + :table_id (mt/id :users) + :name "LAST_LOGIN" + :display_name "Last Login" + :database_type "TIMESTAMP" + :base_type "type/DateTime" + :visibility_type "normal" + :dimension_options (var-get #'table-api/datetime-dimension-indexes) + :default_dimension_option (var-get #'table-api/date-default-index) + :has_field_values "none" + :position 2 + :database_position 2) + (assoc (field-details (Field :table_id (mt/id :users), :name "PASSWORD")) + :special_type "type/Category" + :table_id (mt/id :users) + :name "PASSWORD" + :display_name "Password" + :database_type "VARCHAR" + :base_type "type/Text" + :visibility_type "sensitive" + :has_field_values "list" + :position 3 + :database_position 3)] + :id (mt/id :users)}) + ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata?include_sensitive_fields=true" (mt/id :users)))) + "Make sure that getting the User table *does* include info about the password field, but not actual values themselves")))) + (deftest sensitive-fields-not-included-test - (testing "Sensitive fields should not be included" - (is (= (merge - (query-metadata-defaults) - (db/select-one [Table :created_at :updated_at :fields_hash] :id (mt/id :users)) - {:schema "PUBLIC" - :name "USERS" - :display_name "Users" - :entity_type "entity/UserTable" - :fields [(assoc (field-details (Field (mt/id :users :id))) - :table_id (mt/id :users) - :special_type "type/PK" - :name "ID" - :display_name "ID" - :database_type "BIGINT" - :base_type "type/BigInteger" - :has_field_values "none") - (assoc (field-details (Field (mt/id :users :name))) - :table_id (mt/id :users) - :special_type "type/Name" - :name "NAME" - :display_name "Name" - :database_type "VARCHAR" - :base_type "type/Text" - :has_field_values "list" - :position 1 - :database_position 1) - (assoc (field-details (Field (mt/id :users :last_login))) - :table_id (mt/id :users) - :name "LAST_LOGIN" - :display_name "Last Login" - :database_type "TIMESTAMP" - :base_type "type/DateTime" - :dimension_options (var-get #'table-api/datetime-dimension-indexes) - :default_dimension_option (var-get #'table-api/date-default-index) - :has_field_values "none" - :position 2 - :database_position 2)] - :rows nil - :id (mt/id :users)}) - ((test-users/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :users))))))) - -;; Check that FK fields belonging to Tables we don't have permissions for don't come back as hydrated `:target`(#3867) -(expect - #{{:name "id", :target false} - {:name "fk", :target false}} - ;; create a temp DB with two tables; table-2 has an FK to table-1 - (mt/with-temp* [Database [db] - Table [table-1 {:db_id (u/get-id db)}] - Table [table-2 {:db_id (u/get-id db)}] - Field [table-1-id {:table_id (u/get-id table-1), :name "id", :base_type :type/Integer, :special_type :type/PK}] - Field [table-2-id {:table_id (u/get-id table-2), :name "id", :base_type :type/Integer, :special_type :type/PK}] - Field [table-2-fk {:table_id (u/get-id table-2), :name "fk", :base_type :type/Integer, :special_type :type/FK, :fk_target_field_id (u/get-id table-1-id)}]] - ;; grant permissions only to table-2 - (perms/revoke-permissions! (perms-group/all-users) (u/get-id db)) - (perms/grant-permissions! (perms-group/all-users) (u/get-id db) (:schema table-2) (u/get-id table-2)) - ;; metadata for table-2 should show all fields for table-2, but the FK target info shouldn't be hydrated - (set (for [field (:fields ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (u/get-id table-2))))] - (-> (select-keys field [:name :target]) - (update :target boolean)))))) + (testing "GET api/table/:id/query_metadata" + (testing "Sensitive fields should not be included" + (is (= (merge + (query-metadata-defaults) + (db/select-one [Table :created_at :updated_at :fields_hash] :id (mt/id :users)) + {:schema "PUBLIC" + :name "USERS" + :display_name "Users" + :entity_type "entity/UserTable" + :fields [(assoc (field-details (Field (mt/id :users :id))) + :table_id (mt/id :users) + :special_type "type/PK" + :name "ID" + :display_name "ID" + :database_type "BIGINT" + :base_type "type/BigInteger" + :has_field_values "none") + (assoc (field-details (Field (mt/id :users :name))) + :table_id (mt/id :users) + :special_type "type/Name" + :name "NAME" + :display_name "Name" + :database_type "VARCHAR" + :base_type "type/Text" + :has_field_values "list" + :position 1 + :database_position 1) + (assoc (field-details (Field (mt/id :users :last_login))) + :table_id (mt/id :users) + :name "LAST_LOGIN" + :display_name "Last Login" + :database_type "TIMESTAMP" + :base_type "type/DateTime" + :dimension_options (var-get #'table-api/datetime-dimension-indexes) + :default_dimension_option (var-get #'table-api/date-default-index) + :has_field_values "none" + :position 2 + :database_position 2)] + :id (mt/id :users)}) + ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :users)))) + "Make sure that getting the User table does *not* include password info")))) + +(deftest fk-target-permissions-test + (testing "GET /api/table/:id/query_metadata" + (testing (str "Check that FK fields belonging to Tables we don't have permissions for don't come back as hydrated " + "`:target`(#3867)") + ;; create a temp DB with two tables; table-2 has an FK to table-1 + (mt/with-temp* [Database [db] + Table [table-1 {:db_id (u/get-id db)}] + Table [table-2 {:db_id (u/get-id db)}] + Field [table-1-id {:table_id (u/get-id table-1), :name "id", :base_type :type/Integer, :special_type :type/PK}] + Field [table-2-id {:table_id (u/get-id table-2), :name "id", :base_type :type/Integer, :special_type :type/PK}] + Field [table-2-fk {:table_id (u/get-id table-2), :name "fk", :base_type :type/Integer, :special_type :type/FK, :fk_target_field_id (u/get-id table-1-id)}]] + ;; grant permissions only to table-2 + (perms/revoke-permissions! (perms-group/all-users) (u/get-id db)) + (perms/grant-permissions! (perms-group/all-users) (u/get-id db) (:schema table-2) (u/get-id table-2)) + ;; metadata for table-2 should show all fields for table-2, but the FK target info shouldn't be hydrated + (is (= #{{:name "id", :target false} + {:name "fk", :target false}} + (set (for [field (:fields ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (u/get-id table-2))))] + (-> (select-keys field [:name :target]) + (update :target boolean)))))))))) (deftest update-table-test (testing "PUT /api/table/:id" @@ -332,7 +275,6 @@ {:description "What a nice table!" :entity_type nil :visibility_type "hidden" - :rows nil :display_name "Userz" :pk_field (table/pk-field-id table)}) (dissoc ((mt/user->client :crowberto) :get 200 (format "table/%d" (u/get-id table))) @@ -367,52 +309,87 @@ (is (= 2 @called))))))) -;; ## GET /api/table/:id/fks -;; We expect a single FK from CHECKINS.USER_ID -> USERS.ID -(expect - (let [checkins-user-field (Field (mt/id :checkins :user_id)) - users-id-field (Field (mt/id :users :id)) - fk-field-defaults (dissoc field-defaults :target :dimension_options :default_dimension_option)] - [{:origin_id (:id checkins-user-field) - :destination_id (:id users-id-field) - :relationship "Mt1" - :origin (-> (fk-field-details checkins-user-field) - (dissoc :target :dimensions :values) - (assoc :table_id (mt/id :checkins) - :name "USER_ID" - :display_name "User ID" - :database_type "INTEGER" - :base_type "type/Integer" - :special_type "type/FK" - :database_position 2 - :position 2 - :table (merge - (dissoc (table-defaults) :segments :field_values :metrics) - (db/select-one [Table :id :created_at :updated_at :fields_hash] - :id (mt/id :checkins)) - {:schema "PUBLIC" - :name "CHECKINS" - :display_name "Checkins" - :entity_type "entity/EventTable" - :rows nil}))) - :destination (-> (fk-field-details users-id-field) - (dissoc :target :dimensions :values) - (assoc :table_id (mt/id :users) - :name "ID" - :display_name "ID" - :base_type "type/BigInteger" - :database_type "BIGINT" - :special_type "type/PK" - :table (merge - (dissoc (table-defaults) :db :segments :field_values :metrics) - (db/select-one [Table :id :created_at :updated_at :fields_hash] - :id (mt/id :users)) - {:schema "PUBLIC" - :name "USERS" - :display_name "Users" - :entity_type "entity/UserTable" - :rows nil})))}]) - ((mt/user->client :rasta) :get 200 (format "table/%d/fks" (mt/id :users)))) +(deftest get-fks-test + (testing "GET /api/table/:id/fks" + (testing "We expect a single FK from CHECKINS.USER_ID -> USERS.ID" + (let [checkins-user-field (Field (mt/id :checkins :user_id)) + users-id-field (Field (mt/id :users :id)) + fk-field-defaults (dissoc (field-defaults) :target :dimension_options :default_dimension_option)] + (is (= [{:origin_id (:id checkins-user-field) + :destination_id (:id users-id-field) + :relationship "Mt1" + :origin (-> (fk-field-details checkins-user-field) + (dissoc :target :dimensions :values) + (assoc :table_id (mt/id :checkins) + :name "USER_ID" + :display_name "User ID" + :database_type "INTEGER" + :base_type "type/Integer" + :special_type "type/FK" + :database_position 2 + :position 2 + :table (merge + (dissoc (table-defaults) :segments :field_values :metrics) + (db/select-one [Table :id :created_at :updated_at :fields_hash] + :id (mt/id :checkins)) + {:schema "PUBLIC" + :name "CHECKINS" + :display_name "Checkins" + :entity_type "entity/EventTable"}))) + :destination (-> (fk-field-details users-id-field) + (dissoc :target :dimensions :values) + (assoc :table_id (mt/id :users) + :name "ID" + :display_name "ID" + :base_type "type/BigInteger" + :database_type "BIGINT" + :special_type "type/PK" + :table (merge + (dissoc (table-defaults) :db :segments :field_values :metrics) + (db/select-one [Table :id :created_at :updated_at :fields_hash] + :id (mt/id :users)) + {:schema "PUBLIC" + :name "USERS" + :display_name "Users" + :entity_type "entity/UserTable"})))}] + ((mt/user->client :rasta) :get 200 (format "table/%d/fks" (mt/id :users))))))) + + (testing "should just return nothing for 'virtual' tables" + (is (= [] + ((mt/user->client :crowberto) :get 200 "table/card__1000/fks")))))) + +(deftest basic-query-metadata-test + (testing "GET /api/table/:id/query_metadata" + (is (= (merge + (query-metadata-defaults) + (db/select-one [Table :created_at :updated_at :fields_hash] :id (mt/id :categories)) + {:schema "PUBLIC" + :name "CATEGORIES" + :display_name "Categories" + :fields [(merge + (field-details (Field (mt/id :categories :id))) + {:table_id (mt/id :categories) + :special_type "type/PK" + :name "ID" + :display_name "ID" + :database_type "BIGINT" + :base_type "type/BigInteger" + :has_field_values "none"}) + (merge + (field-details (Field (mt/id :categories :name))) + {:table_id (mt/id :categories) + :special_type "type/Name" + :name "NAME" + :display_name "Name" + :database_type "VARCHAR" + :base_type "type/Text" + :dimension_options [] + :default_dimension_option nil + :has_field_values "list" + :database_position 1 + :position 1})] + :id (mt/id :categories)}) + ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :categories))))))) (defn- with-field-literal-id [{field-name :name, base-type :base_type :as field}] (assoc field :id ["field-literal" field-name base-type])) @@ -484,49 +461,44 @@ (mt/round-all-decimals 2)))))))) (deftest include-date-dimensions-in-nested-query-test - (testing "Test date dimensions being included with a nested query" - (mt/with-temp Card [card {:name "Users" - :database_id (mt/id) - :dataset_query {:database (mt/id) - :type :native - :native {:query (format "SELECT NAME, LAST_LOGIN FROM USERS")}}}] - (let [card-virtual-table-id (str "card__" (u/get-id card))] - ;; run the Card which will populate its result_metadata column - ((mt/user->client :crowberto) :post 202 (format "card/%d/query" (u/get-id card))) - ;; Now fetch the metadata for this "table" via the API - (let [[name-metadata last-login-metadata] (db/select-one-field :result_metadata Card :id (u/get-id card))] - (is (= {:display_name "Users" - :schema "Everything else" - :db_id (:database_id card) - :id card-virtual-table-id - :description nil - :dimension_options (default-dimension-options) - :fields [{:name "NAME" - :display_name "NAME" - :base_type "type/Text" - :table_id card-virtual-table-id - :id ["field-literal" "NAME" "type/Text"] - :special_type "type/Name" - :default_dimension_option nil - :dimension_options [] - :fingerprint (:fingerprint name-metadata)} - {:name "LAST_LOGIN" - :display_name "LAST_LOGIN" - :base_type "type/DateTime" - :table_id card-virtual-table-id - :id ["field-literal" "LAST_LOGIN" "type/DateTime"] - :special_type nil - :default_dimension_option (var-get #'table-api/date-default-index) - :dimension_options (var-get #'table-api/datetime-dimension-indexes) - :fingerprint (:fingerprint last-login-metadata)}]} - ((mt/user->client :crowberto) :get 200 - (format "table/card__%d/query_metadata" (u/get-id card)))))))))) - - -;; make sure GET /api/table/:id/fks just returns nothing for 'virtual' tables -(expect - [] - ((mt/user->client :crowberto) :get 200 "table/card__1000/fks")) + (testing "GET /api/table/:id/query_metadata" + (testing "Test date dimensions being included with a nested query" + (mt/with-temp Card [card {:name "Users" + :database_id (mt/id) + :dataset_query {:database (mt/id) + :type :native + :native {:query (format "SELECT NAME, LAST_LOGIN FROM USERS")}}}] + (let [card-virtual-table-id (str "card__" (u/get-id card))] + ;; run the Card which will populate its result_metadata column + ((mt/user->client :crowberto) :post 202 (format "card/%d/query" (u/get-id card))) + ;; Now fetch the metadata for this "table" via the API + (let [[name-metadata last-login-metadata] (db/select-one-field :result_metadata Card :id (u/get-id card))] + (is (= {:display_name "Users" + :schema "Everything else" + :db_id (:database_id card) + :id card-virtual-table-id + :description nil + :dimension_options (default-dimension-options) + :fields [{:name "NAME" + :display_name "NAME" + :base_type "type/Text" + :table_id card-virtual-table-id + :id ["field-literal" "NAME" "type/Text"] + :special_type "type/Name" + :default_dimension_option nil + :dimension_options [] + :fingerprint (:fingerprint name-metadata)} + {:name "LAST_LOGIN" + :display_name "LAST_LOGIN" + :base_type "type/DateTime" + :table_id card-virtual-table-id + :id ["field-literal" "LAST_LOGIN" "type/DateTime"] + :special_type nil + :default_dimension_option (var-get #'table-api/date-default-index) + :dimension_options (var-get #'table-api/datetime-dimension-indexes) + :fingerprint (:fingerprint last-login-metadata)}]} + ((mt/user->client :crowberto) :get 200 + (format "table/card__%d/query_metadata" (u/get-id card))))))))))) (defn- narrow-fields [category-names api-response] (for [field (:fields api-response) @@ -545,84 +517,69 @@ (mt/with-temp-vals-in-db Field (mt/id :venues :category_id) {:special_type special-type} (f))) -;; ## GET /api/table/:id/query_metadata -;; Ensure internal remapped dimensions and human_readable_values are returned -(expect - [{:table_id (mt/id :venues) - :id (mt/id :venues :category_id) - :name "CATEGORY_ID" - :dimensions {:name "Foo", :field_id (mt/id :venues :category_id), :human_readable_field_id nil, :type "internal"}} - {:id (mt/id :venues :price) - :table_id (mt/id :venues) - :name "PRICE" - :dimensions []}] - (mt/with-temp-objects - (data/create-venue-category-remapping "Foo") - (category-id-special-type - :type/Category - (fn [] - (narrow-fields ["PRICE" "CATEGORY_ID"] - ((test-users/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))))))) - -;; ## GET /api/table/:id/query_metadata -;; Ensure internal remapped dimensions and human_readable_values are returned when type is enum -(expect - [{:table_id (mt/id :venues) - :id (mt/id :venues :category_id) - :name "CATEGORY_ID" - :dimensions {:name "Foo", :field_id (mt/id :venues :category_id), :human_readable_field_id nil, :type "internal"}} - {:id (mt/id :venues :price) - :table_id (mt/id :venues) - :name "PRICE" - :dimensions []}] - (mt/with-temp-objects - (data/create-venue-category-remapping "Foo") - (category-id-special-type - :type/Enum - (fn [] - (narrow-fields ["PRICE" "CATEGORY_ID"] - ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))))))) - -;; ## GET /api/table/:id/query_metadata -;; Ensure FK remappings are returned -(expect - [{:table_id (mt/id :venues) - :id (mt/id :venues :category_id) - :name "CATEGORY_ID" - :dimensions {:name "Foo", :field_id (mt/id :venues :category_id), :human_readable_field_id (mt/id :categories :name), :type "external"}} - {:id (mt/id :venues :price) - :table_id (mt/id :venues) - :name "PRICE" - :dimensions []}] - (mt/with-temp-objects - (data/create-venue-category-fk-remapping "Foo") - (category-id-special-type - :type/Category - (fn [] - (narrow-fields ["PRICE" "CATEGORY_ID"] - ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))))))) - -;; Ensure dimensions options are sorted numerically, but returned as strings -(expect - (map str (sort (map #(Long/parseLong %) (var-get #'table-api/datetime-dimension-indexes)))) - (var-get #'table-api/datetime-dimension-indexes)) - -(expect - (map str (sort (map #(Long/parseLong %) (var-get #'table-api/numeric-dimension-indexes)))) - (var-get #'table-api/numeric-dimension-indexes)) - -;; Numeric fields without min/max values should not have binning strategies -(expect - [] - (let [fingerprint (db/select-one-field :fingerprint Field {:id (mt/id :venues :latitude)}) - temp-fingerprint (-> fingerprint - (assoc-in [:type :type/Number :max] nil) - (assoc-in [:type :type/Number :min] nil))] - (mt/with-temp-vals-in-db Field (mt/id :venues :latitude) {:fingerprint temp-fingerprint} - (-> ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :categories))) - (get-in [:fields]) - first - :dimension_options)))) +(deftest query-metadata-remappings-test + (testing "GET /api/table/:id/query_metadata" + (mt/with-temp-objects + (data/create-venue-category-remapping "Foo") + (testing "Ensure internal remapped dimensions and human_readable_values are returned" + (is (= [{:table_id (mt/id :venues) + :id (mt/id :venues :category_id) + :name "CATEGORY_ID" + :dimensions {:name "Foo", :field_id (mt/id :venues :category_id), :human_readable_field_id nil, :type "internal"}} + {:id (mt/id :venues :price) + :table_id (mt/id :venues) + :name "PRICE" + :dimensions []}] + (category-id-special-type + :type/Category + (fn [] + (narrow-fields ["PRICE" "CATEGORY_ID"] + ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues))))))))) + + (testing "Ensure internal remapped dimensions and human_readable_values are returned when type is enum" + (is (= [{:table_id (mt/id :venues) + :id (mt/id :venues :category_id) + :name "CATEGORY_ID" + :dimensions {:name "Foo", :field_id (mt/id :venues :category_id), :human_readable_field_id nil, :type "internal"}} + {:id (mt/id :venues :price) + :table_id (mt/id :venues) + :name "PRICE" + :dimensions []}] + (category-id-special-type + :type/Enum + (fn [] + (narrow-fields ["PRICE" "CATEGORY_ID"] + ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))))))))) + + (mt/with-temp-objects + (data/create-venue-category-fk-remapping "Foo") + (testing "Ensure FK remappings are returned" + (is (= [{:table_id (mt/id :venues) + :id (mt/id :venues :category_id) + :name "CATEGORY_ID" + :dimensions {:name "Foo" + :field_id (mt/id :venues :category_id) + :human_readable_field_id (mt/id :categories :name) + :type "external"}} + {:id (mt/id :venues :price) + :table_id (mt/id :venues) + :name "PRICE" + :dimensions []}] + (category-id-special-type + :type/Category + (fn [] + (narrow-fields ["PRICE" "CATEGORY_ID"] + ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))))))))))) + +(deftest dimension-options-sort-test + (testing "Ensure dimensions options are sorted numerically, but returned as strings" + (testing "datetime indexes" + (is (= (map str (sort (map #(Long/parseLong %) (var-get #'table-api/datetime-dimension-indexes)))) + (var-get #'table-api/datetime-dimension-indexes)))) + + (testing "numeric indexes" + (is (= (map str (sort (map #(Long/parseLong %) (var-get #'table-api/numeric-dimension-indexes)))) + (var-get #'table-api/numeric-dimension-indexes)))))) (defn- dimension-options-for-field [response, ^String field-name] (->> response @@ -638,22 +595,31 @@ :let [{[_ _ strategy _] :mbql} (get-in response [:dimension_options (keyword dim-index)])]] strategy))) -;; Lat/Long fields should use bin-width rather than num-bins -(expect - (if (data/binning-supported?) - #{nil "bin-width" "default"} - #{}) - (let [response ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))] - (extract-dimension-options response "latitude"))) - -;; Number columns without a special type should use "num-bins" -(expect - (if (data/binning-supported?) - #{nil "num-bins" "default"} - #{}) - (mt/with-temp-vals-in-db Field (mt/id :venues :price) {:special_type nil} - (let [response ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))] - (extract-dimension-options response "price")))) +(deftest numeric-binning-options-test + (testing "GET /api/table/:id/query_metadata" + (testing "binning options for numeric fields" + (testing "Lat/Long fields should use bin-width rather than num-bins" + (let [response ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))] + (is (= #{nil "bin-width" "default"} + (extract-dimension-options response "latitude"))))) + + (testing "Number columns without a special type should use \"num-bins\"" + (mt/with-temp-vals-in-db Field (mt/id :venues :price) {:special_type nil} + (let [response ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :venues)))] + (is (= #{nil "num-bins" "default"} + (extract-dimension-options response "price")))))) + + (testing "Numeric fields without min/max values should not have binning options" + (let [fingerprint (db/select-one-field :fingerprint Field {:id (mt/id :venues :latitude)}) + temp-fingerprint (-> fingerprint + (assoc-in [:type :type/Number :max] nil) + (assoc-in [:type :type/Number :min] nil))] + (mt/with-temp-vals-in-db Field (mt/id :venues :latitude) {:fingerprint temp-fingerprint} + (is (= [] + (-> ((mt/user->client :rasta) :get 200 (format "table/%d/query_metadata" (mt/id :categories))) + (get-in [:fields]) + first + :dimension_options))))))))) (deftest datetime-binning-options-test (testing "GET /api/table/:id/query_metadata" diff --git a/test/metabase/models/field_values_test.clj b/test/metabase/models/field_values_test.clj index e00c0525bee4701b18649340dee82185f4dbd876..2cf8f538921714c2e03dbef6ed639dd0d9320689 100644 --- a/test/metabase/models/field_values_test.clj +++ b/test/metabase/models/field_values_test.clj @@ -1,95 +1,117 @@ (ns metabase.models.field-values-test "Tests for specific behavior related to FieldValues and functions in the `metabase.models.field-values` namespace." - (:require [clojure.java.jdbc :as jdbc] - [expectations :refer :all] + (:require [clojure + [string :as str] + [test :refer :all]] + [clojure.java.jdbc :as jdbc] [metabase [db :as mdb] [sync :as sync] + [test :as mt] [util :as u]] + [metabase.db.metadata-queries :as metadata-queries] [metabase.models [database :refer [Database]] [field :refer [Field]] - [field-values :refer :all] + [field-values :as field-values :refer :all] [table :refer [Table]]] - [toucan.db :as db] - [toucan.util.test :as tt])) - -;;; ---------------------------------------- field-should-have-field-values? ----------------------------------------- - -(expect (field-should-have-field-values? {:has_field_values :list - :visibility_type :normal - :base_type :type/Text})) - -(expect false (field-should-have-field-values? {:has_field_values :list - :visibility_type :sensitive - :base_type :type/Text})) - -(expect false (field-should-have-field-values? {:has_field_values :list - :visibility_type :hidden - :base_type :type/Text})) - -(expect false (field-should-have-field-values? {:has_field_values :list - :visibility_type :details-only - :base_type :type/Text})) - -(expect false (field-should-have-field-values? {:has_field_values nil - :visibility_type :normal - :base_type :type/Text})) - -(expect (field-should-have-field-values? {:has_field_values :list - :visibility_type :normal - :base_type :type/Text})) - -(expect (field-should-have-field-values? {:has_field_values :list - :special_type :type/Category - :visibility_type :normal - :base_type "type/Boolean"})) - - -;; retired/sensitive/hidden/details-only fields should always be excluded -(expect false (field-should-have-field-values? {:base_type :type/Boolean - :has_field_values :list - :visibility_type :retired})) - -(expect false (field-should-have-field-values? {:base_type :type/Boolean - :has_field_values :list - :visibility_type :sensitive})) - -(expect false (field-should-have-field-values? {:base_type :type/Boolean - :has_field_values :list - :visibility_type :hidden})) - -(expect false (field-should-have-field-values? {:base_type :type/Boolean - :has_field_values :list - :visibility_type :details-only})) - -;; date/time based fields should always be excluded -(expect false (field-should-have-field-values? {:base_type :type/Date - :has_field_values :list - :visibility_type :normal})) - -(expect false (field-should-have-field-values? {:base_type :type/DateTime - :has_field_values :list - :visibility_type :normal})) - -(expect false (field-should-have-field-values? {:base_type :type/Time - :has_field_values :list - :visibility_type :normal})) - - -;;; ------------------------------------------------ everything else ------------------------------------------------- - -(expect - [[1 2 3] - nil] - (tt/with-temp* [Database [{database-id :id}] + [toucan.db :as db])) + +(deftest field-should-have-field-values?-test + (doseq [[group input->expected] {"Text and Category Fields" + {{:has_field_values :list + :visibility_type :normal + :base_type :type/Text} + true + + {:has_field_values nil + :visibility_type :normal + :base_type :type/Text} + false + + {:has_field_values :list + :special_type :type/Category + :visibility_type :normal + :base_type "type/Boolean"} + true} + + "retired/sensitive/hidden/details-only fields should always be excluded" + {{:base_type :type/Boolean + :has_field_values :list + :visibility_type :retired} + false + + {:base_type :type/Boolean + :has_field_values :list + :visibility_type :sensitive} + false + + {:has_field_values :list + :visibility_type :sensitive + :base_type :type/Text} + false + + {:base_type :type/Boolean + :has_field_values :list + :visibility_type :hidden} + false + + {:has_field_values :list + :visibility_type :hidden + :base_type :type/Text} + false + + {:base_type :type/Boolean + :has_field_values :list + :visibility_type :details-only} + false + + {:has_field_values :list + :visibility_type :details-only + :base_type :type/Text} + false} + + "date/time based fields should always be excluded" + {{:base_type :type/Date + :has_field_values :list + :visibility_type :normal} + false + + {:base_type :type/DateTime + :has_field_values :list + :visibility_type :normal} + false + + {:base_type :type/Time + :has_field_values :list + :visibility_type :normal} + false}} + [input expected] input->expected] + (testing (str group "\n") + (testing (pr-str (list 'field-should-have-field-values? input)) + (is (= expected + (#'field-values/field-should-have-field-values? input))))))) + +(deftest distinct-values-test + (with-redefs [metadata-queries/field-distinct-values (constantly [1 2 3 4])] + (is (= [1 2 3 4] + (#'field-values/distinct-values {})))) + + (testing "(#2332) check that if field values are long we skip over them" + (with-redefs [metadata-queries/field-distinct-values (constantly [(str/join (repeat 50000 "A"))])] + (is (= nil + (#'field-values/distinct-values {})))))) + +(deftest clear-field-values!-test + (mt/with-temp* [Database [{database-id :id}] Table [{table-id :id} {:db_id database-id}] Field [{field-id :id} {:table_id table-id}] FieldValues [_ {:field_id field-id, :values "[1,2,3]"}]] - [(db/select-one-field :values FieldValues, :field_id field-id) - (do - (clear-field-values! field-id) - (db/select-one-field :values FieldValues, :field_id field-id))])) + (is (= [1 2 3] + (db/select-one-field :values FieldValues, :field_id field-id))) + (#'field-values/clear-field-values! field-id) + (is (= nil + (db/select-one-field :values FieldValues, :field_id field-id))))) (defn- find-values [field-values-id] (-> (db/select-one FieldValues :id field-values-id) @@ -99,48 +121,52 @@ (sync/sync-database! db) (find-values field-values-id)) -;; Test "fixing" of human readable values when field values change -(expect - (concat (repeat 2 {:values [1 2 3] :human_readable_values ["a" "b" "c"]}) - (repeat 2 {:values [-2 -1 0 1 2 3] :human_readable_values ["-2" "-1" "0" "a" "b" "c"]}) - [{:values [-2 -1 0] :human_readable_values ["-2" "-1" "0"]}]) - - (binding [mdb/*allow-potentailly-unsafe-connections* true] - ;; Create a temp warehouse database that can have it's field values change - (jdbc/with-db-connection [conn {:classname "org.h2.Driver", :subprotocol "h2", :subname "mem:temp"}] - (jdbc/execute! conn ["drop table foo if exists"]) - (jdbc/execute! conn ["create table foo (id integer primary key, category_id integer not null, desc text)"]) - (jdbc/insert-multi! conn :foo [{:id 1 :category_id 1 :desc "foo"} - {:id 2 :category_id 2 :desc "bar"} - {:id 3 :category_id 3 :desc "baz"}]) - - ;; Create a new in the Database table for this newly created temp database - (tt/with-temp Database [db {:engine :h2 - :name "foo" - :is_full_sync true - :details "{\"db\": \"mem:temp\"}"}] - - ;; Sync the database so we have the new table and it's fields - (do (sync/sync-database! db) - (let [table-id (db/select-one-field :id Table :db_id (u/get-id db) :name "FOO") - field-id (db/select-one-field :id Field :table_id table-id :name "CATEGORY_ID") - field-values-id (db/select-one-field :id FieldValues :field_id field-id)] - ;; Add in human readable values for remapping - (db/update! FieldValues field-values-id {:human_readable_values "[\"a\",\"b\",\"c\"]"}) - - ;; This is the starting point, the original catgory ids and their remapped values - [(find-values field-values-id) - ;; There should be no changes to human_readable_values when resync'd - (sync-and-find-values db field-values-id) - (do - ;; Add new rows that will have new field values - (jdbc/insert-multi! conn :foo [{:id 4 :category_id -2 :desc "foo"} - {:id 5 :category_id -1 :desc "bar"} - {:id 6 :category_id 0 :desc "baz"}]) - ;; Sync to pickup the new field values and rebuild the human_readable_values - (sync-and-find-values db field-values-id)) - ;; Resyncing this (with the new field values) should result in the same human_readable_values - (sync-and-find-values db field-values-id) - ;; Test that field values can be removed and the corresponding human_readable_values are removed as well - (do (jdbc/delete! conn :foo ["id in (?,?,?)" 1 2 3]) - (sync-and-find-values db field-values-id))])))))) +(deftest update-human-readable-values-test + (testing "Test \"fixing\" of human readable values when field values change" + (binding [mdb/*allow-potentailly-unsafe-connections* true] + ;; Create a temp warehouse database that can have it's field values change + (jdbc/with-db-connection [conn {:classname "org.h2.Driver", :subprotocol "h2", :subname "mem:temp"}] + (jdbc/execute! conn ["drop table foo if exists"]) + (jdbc/execute! conn ["create table foo (id integer primary key, category_id integer not null, desc text)"]) + (jdbc/insert-multi! conn :foo [{:id 1 :category_id 1 :desc "foo"} + {:id 2 :category_id 2 :desc "bar"} + {:id 3 :category_id 3 :desc "baz"}]) + ;; Create a new in the Database table for this newly created temp database + (mt/with-temp Database [db {:engine :h2 + :name "foo" + :is_full_sync true + :details "{\"db\": \"mem:temp\"}"}] + ;; Sync the database so we have the new table and it's fields + (sync/sync-database! db) + (let [table-id (db/select-one-field :id Table :db_id (u/get-id db) :name "FOO") + field-id (db/select-one-field :id Field :table_id table-id :name "CATEGORY_ID") + field-values-id (db/select-one-field :id FieldValues :field_id field-id)] + ;; Add in human readable values for remapping + (db/update! FieldValues field-values-id {:human_readable_values "[\"a\",\"b\",\"c\"]"}) + (let [expected-original-values {:values [1 2 3] + :human_readable_values ["a" "b" "c"]} + expected-updated-values {:values [-2 -1 0 1 2 3] + :human_readable_values ["-2" "-1" "0" "a" "b" "c"]}] + (is (= expected-original-values + (find-values field-values-id))) + + (testing "There should be no changes to human_readable_values when resync'd" + (is (= expected-original-values + (sync-and-find-values db field-values-id)))) + + (testing "Add new rows that will have new field values" + (jdbc/insert-multi! conn :foo [{:id 4 :category_id -2 :desc "foo"} + {:id 5 :category_id -1 :desc "bar"} + {:id 6 :category_id 0 :desc "baz"}]) + (testing "Sync to pickup the new field values and rebuild the human_readable_values" + (is (= expected-updated-values + (sync-and-find-values db field-values-id))))) + + (testing "Resyncing this (with the new field values) should result in the same human_readable_values" + (is (= expected-updated-values + (sync-and-find-values db field-values-id)))) + + (testing "Test that field values can be removed and the corresponding human_readable_values are removed as well" + (jdbc/delete! conn :foo ["id in (?,?,?)" 1 2 3]) + (is (= {:values [-2 -1 0] :human_readable_values ["-2" "-1" "0"]} + (sync-and-find-values db field-values-id))))))))))) diff --git a/test/metabase/sync/analyze/table_row_count_test.clj b/test/metabase/sync/analyze/table_row_count_test.clj deleted file mode 100644 index 14b879f3a43874bb52389ea42098d2a0f4a8c959..0000000000000000000000000000000000000000 --- a/test/metabase/sync/analyze/table_row_count_test.clj +++ /dev/null @@ -1,19 +0,0 @@ -(ns metabase.sync.analyze.table-row-count-test - "Tests for the sync logic that updates a Table's row count." - (:require [metabase.models.table :refer [Table]] - [metabase.sync.analyze.table-row-count :as table-row-count] - [metabase.test :as mt] - [metabase.test - [data :as data] - [util :as tu]] - [metabase.test.data.datasets :as datasets] - [toucan.db :as db])) - -;; test that syncing table row counts works -;; TODO - write a Druid version of this test. Works slightly differently since Druid doesn't have a 'venues' table -;; TODO - not sure why this doesn't work on Oracle. Seems to be an issue with the test rather than with the Oracle driver -(datasets/expect-with-drivers (mt/normal-drivers-except #{:oracle}) - 100 - (tu/with-temp-vals-in-db Table (data/id :venues) {:rows 0} - (table-row-count/update-row-count! (Table (data/id :venues))) - (db/select-one-field :rows Table :id (data/id :venues)))) diff --git a/test/metabase/sync_database/analyze_test.clj b/test/metabase/sync_database/analyze_test.clj index 275886a3fc4b091c0dbf96eca4922b27f0d3e693..ea4529cdbc7f2db777534e156645e9c71af9a9cc 100644 --- a/test/metabase/sync_database/analyze_test.clj +++ b/test/metabase/sync_database/analyze_test.clj @@ -1,82 +1,67 @@ (ns metabase.sync-database.analyze-test "TODO - this namespace follows the old pattern of sync namespaces. Tests should be moved to appropriate new homes at some point" - (:require [clojure.string :as str] - [expectations :refer :all] - [metabase.db.metadata-queries :as metadata-queries] + (:require [clojure.test :refer :all] + [metabase + [test :as mt] + [util :as u]] [metabase.models [database :refer [Database]] [field :as field :refer [Field]] - [field-values :as field-values] [table :as table :refer [Table]]] [metabase.sync.analyze :as analyze] [metabase.sync.analyze.classifiers.text-fingerprint :as classify-text-fingerprint] [metabase.sync.analyze.fingerprint.fingerprinters :as fingerprinters] - [metabase.test.data :as data] [metabase.test.data.users :refer :all] - [metabase.util :as u] - [toucan.db :as db] - [toucan.util.test :as tt])) + [toucan.db :as db])) -;; distinct-values -;; (#2332) check that if field values are long we skip over them -;; TODO - the next two should probably be moved into field-values-test -(expect - nil - (with-redefs [metadata-queries/field-distinct-values (constantly [(str/join (repeat 50000 "A"))])] - (#'field-values/distinct-values {}))) - -(expect - [1 2 3 4] - (with-redefs [metadata-queries/field-distinct-values (constantly [1 2 3 4])] - (#'field-values/distinct-values {}))) - - -;;; ## mark-json-field! - -(defn- values-are-valid-json? [values] +(defn- classified-special-type [values] (let [field (field/map->FieldInstance {:base_type :type/Text})] - (= (:special_type (classify-text-fingerprint/infer-special-type field (transduce identity (fingerprinters/fingerprinter field) values))) - :type/SerializedJSON))) - -;; When all the values are valid JSON dicts they're valid JSON -(expect - (values-are-valid-json? ["{\"this\":\"is\",\"valid\":\"json\"}" - "{\"this\":\"is\",\"valid\":\"json\"}" - "{\"this\":\"is\",\"valid\":\"json\"}"])) - -;; When all the values are valid JSON arrays they're valid JSON -(expect - (values-are-valid-json? ["[1, 2, 3, 4]" - "[1, 2, 3, 4]" - "[1, 2, 3, 4]"])) - -;; Some combo of both can still be marked as JSON -(expect - (values-are-valid-json? ["{\"this\":\"is\",\"valid\":\"json\"}" - "[1, 2, 3, 4]" - "[1, 2, 3, 4]"])) - -;; Check that things that aren't dictionaries or arrays aren't marked as JSON -(expect false (values-are-valid-json? ["\"A JSON string should not cause a Field to be marked as JSON\""])) -(expect false (values-are-valid-json? ["100"])) -(expect false (values-are-valid-json? ["true"])) -(expect false (values-are-valid-json? ["false"])) - -;; Check that things that are valid emails are marked as Emails - -(defn- values-are-valid-emails? [values] - (let [field (field/map->FieldInstance {:base_type :type/Text})] - (= (:special_type (classify-text-fingerprint/infer-special-type field (transduce identity (fingerprinters/fingerprinter field) values))) - :type/Email))) - -(expect true (values-are-valid-emails? ["helper@metabase.com"])) -(expect true (values-are-valid-emails? ["helper@metabase.com", "someone@here.com", "help@nope.com"])) - -(expect false (values-are-valid-emails? ["helper@metabase.com", "1111IsNot!An....email", "help@nope.com"])) -(expect false (values-are-valid-emails? ["\"A string should not cause a Field to be marked as email\""])) -(expect false (values-are-valid-emails? ["true"])) -(expect false (values-are-valid-emails? ["false"])) + (println "RESULT =" (classify-text-fingerprint/infer-special-type + field + (transduce identity (fingerprinters/fingerprinter field) values))) + (:special_type (classify-text-fingerprint/infer-special-type + field + (transduce identity (fingerprinters/fingerprinter field) values))))) + +(deftest classify-json-test + (doseq [[group values->expected] {"When all the values are valid JSON dicts they're valid JSON" + {["{\"this\":\"is\",\"valid\":\"json\"}" + "{\"this\":\"is\",\"valid\":\"json\"}" + "{\"this\":\"is\",\"valid\":\"json\"}"] true} + + "When all the values are valid JSON arrays they're valid JSON" + {["[1, 2, 3, 4]" + "[1, 2, 3, 4]" + "[1, 2, 3, 4]"] true} + + "Some combo of both can still be marked as JSON" + {["{\"this\":\"is\",\"valid\":\"json\"}" + "[1, 2, 3, 4]" + "[1, 2, 3, 4]"] true} + + "Check that things that aren't dictionaries or arrays aren't marked as JSON" + {["\"A JSON string should not cause a Field to be marked as JSON\""] false + ["100"] false + ["true"] false + ["false"] false}} + [values expected] values->expected] + (testing (str group "\n") + (testing (pr-str values) + (is (= (when expected :type/SerializedJSON) + (classified-special-type values))))))) + +(deftest classify-emails-test + (testing "Check that things that are valid emails are marked as Emails") + (doseq [[values expected] {["helper@metabase.com"] true + ["helper@metabase.com", "someone@here.com", "help@nope.com"] true + ["helper@metabase.com", "1111IsNot!An....email", "help@nope.com"] false + ["\"A string should not cause a Field to be marked as email\""] false + ["true"] false + ["false"] false}] + (testing (pr-str values) + (is (= (when expected :type/Email) + (classified-special-type values)))))) ;;; +----------------------------------------------------------------------------------------------------------------+ @@ -96,84 +81,85 @@ {:order-by [[:last_analyzed :desc]]})) (defn- set-table-visibility-type-via-api! - "Change the VISIBILITY-TYPE of TABLE via an API call. - (This is done via the API so we can see which, if any, side effects (e.g. analysis) get triggered.)" + "Change the `visibility-type` of `table` via an API call. (This is done via the API so we can see which, if any, side + effects (e.g. analysis) get triggered.)" [table visibility-type] ((user->client :crowberto) :put 200 (format "table/%d" (:id table)) {:display_name "hiddentable" :visibility_type visibility-type :description "What a nice table!"})) (defn- api-sync! - "Trigger a sync of TABLE via the API." + "Trigger a sync of `table` via the API." [table] ((user->client :crowberto) :post 200 (format "database/%d/sync" (:db_id table)))) ;; use these functions to create fake Tables & Fields that are actually backed by something real in the database. ;; Otherwise when we go to resync them the logic will figure out Table/Field doesn't exist and mark it as inactive (defn- fake-table [& {:as additional-options}] - (merge {:rows 15, :db_id (data/id), :name "VENUES"} + (merge {:db_id (mt/id), :name "VENUES"} additional-options)) (defn- fake-field [table & {:as additional-options}] (merge {:table_id (u/get-id table), :name "PRICE", :base_type "type/Integer"} additional-options)) -;; expect all the kinds of hidden tables to stay un-analyzed through transitions and repeated syncing -(expect - false - (tt/with-temp* [Table [table (fake-table)] - Field [field (fake-field table)]] - (set-table-visibility-type-via-api! table "hidden") - (api-sync! table) - (set-table-visibility-type-via-api! table "cruft") - (set-table-visibility-type-via-api! table "cruft") - (api-sync! table) - (set-table-visibility-type-via-api! table "technical") - (api-sync! table) - (set-table-visibility-type-via-api! table "technical") - (api-sync! table) - (api-sync! table) - (fake-field-was-analyzed? field))) - -;; same test not coming through the api (defn- analyze-table! [table] ;; we're calling `analyze-db!` instead of `analyze-table!` because the latter doesn't care if you try to sync a ;; hidden table and will allow that. TODO - Does that behavior make sense? (analyze/analyze-db! (Database (:db_id table)))) -(expect - false - (tt/with-temp* [Table [table (fake-table)] - Field [field (fake-field table)]] - (set-table-visibility-type-via-api! table "hidden") - (analyze-table! table) - (set-table-visibility-type-via-api! table "cruft") - (set-table-visibility-type-via-api! table "cruft") - (analyze-table! table) - (set-table-visibility-type-via-api! table "technical") - (analyze-table! table) - (set-table-visibility-type-via-api! table "technical") - (analyze-table! table) - (analyze-table! table) - (fake-field-was-analyzed? field))) - -;; un-hiding a table should cause it to be analyzed -(expect - (tt/with-temp* [Table [table (fake-table)] - Field [field (fake-field table)]] - (set-table-visibility-type-via-api! table "hidden") - (set-table-visibility-type-via-api! table nil) - (fake-field-was-analyzed? field))) - -;; re-hiding a table should not cause it to be analyzed -(expect - ;; create an initially hidden table - (tt/with-temp* [Table [table (fake-table :visibility_type "hidden")] - Field [field (fake-field table)]] - ;; switch the table to visible (triggering a sync) and get the last sync time - (let [last-sync-time (do (set-table-visibility-type-via-api! table nil) - (latest-sync-time table))] - ;; now make it hidden again +(deftest dont-analyze-hidden-tables-test + (testing "expect all the kinds of hidden tables to stay un-analyzed through transitions and repeated syncing" + (mt/with-temp* [Table [table (fake-table)] + Field [field (fake-field table)]] + (set-table-visibility-type-via-api! table "hidden") + (api-sync! table) + (set-table-visibility-type-via-api! table "cruft") + (set-table-visibility-type-via-api! table "cruft") + (api-sync! table) + (set-table-visibility-type-via-api! table "technical") + (api-sync! table) + (set-table-visibility-type-via-api! table "technical") + (api-sync! table) + (api-sync! table) + (is (= false + (fake-field-was-analyzed? field)))) + + (testing "same test not coming through the api" + (mt/with-temp* [Table [table (fake-table)] + Field [field (fake-field table)]] + (set-table-visibility-type-via-api! table "hidden") + (analyze-table! table) + (set-table-visibility-type-via-api! table "cruft") + (set-table-visibility-type-via-api! table "cruft") + (analyze-table! table) + (set-table-visibility-type-via-api! table "technical") + (analyze-table! table) + (set-table-visibility-type-via-api! table "technical") + (analyze-table! table) + (analyze-table! table) + (is (= false + (fake-field-was-analyzed? field))))))) + +(deftest analyze-unhidden-tables-test + (testing "un-hiding a table should cause it to be analyzed" + (mt/with-temp* [Table [table (fake-table)] + Field [field (fake-field table)]] (set-table-visibility-type-via-api! table "hidden") - ;; sync time shouldn't change - (= last-sync-time (latest-sync-time table))))) + (set-table-visibility-type-via-api! table nil) + (is (= true + (fake-field-was-analyzed? field)))))) + +(deftest dont-analyze-rehidden-table-test + (testing "re-hiding a table should not cause it to be analyzed" + ;; create an initially hidden table + (mt/with-temp* [Table [table (fake-table :visibility_type "hidden")] + Field [field (fake-field table)]] + ;; switch the table to visible (triggering a sync) and get the last sync time + (let [last-sync-time (do (set-table-visibility-type-via-api! table nil) + (latest-sync-time table))] + ;; now make it hidden again + (set-table-visibility-type-via-api! table "hidden") + (is (= last-sync-time + (latest-sync-time table)) + "sync time shouldn't change"))))) diff --git a/test/metabase/sync_database_test.clj b/test/metabase/sync_database_test.clj index 5e921c3ed9e7e6a0c1e8d385ce41782eaeb24781..0b9e8a8fc1cf5b42aaeb193bebb64fa652b7de9e 100644 --- a/test/metabase/sync_database_test.clj +++ b/test/metabase/sync_database_test.clj @@ -93,56 +93,39 @@ (update :fingerprint_version (complement zero?)))))) tu/boolean-ids-and-timestamps))) -(def ^:private table-defaults - {:active true - :caveats nil - :created_at true - :db_id true - :description nil - :entity_name nil - :entity_type :entity/GenericTable - :id true - :points_of_interest nil - :rows nil - :schema nil - :show_in_getting_started false - :updated_at true - :visibility_type nil - :fields_hash true - :field_order :database}) - -(def ^:private field-defaults - {:active true - :caveats nil - :created_at true - :description nil - :fingerprint false - :fingerprint_version false - :fk_target_field_id false - :has_field_values nil - :id true - :last_analyzed false - :parent_id false - :points_of_interest nil - :position 0 - :database_position 0 - :custom_position 0 - :preview_display true - :special_type nil - :table_id true - :updated_at true - :visibility_type :normal - :settings nil}) - -(def ^:private field-defaults-with-fingerprint - (assoc field-defaults +(defn- table-defaults [] + (merge + (mt/object-defaults Table) + {:created_at true + :db_id true + :entity_type :entity/GenericTable + :id true + :updated_at true + :fields_hash true})) + +(defn- field-defaults [] + (merge + (mt/object-defaults Field) + {:created_at true + :fingerprint false + :fingerprint_version false + :fk_target_field_id false + :id true + :last_analyzed false + :parent_id false + :position 0 + :table_id true + :updated_at true})) + +(defn- field-defaults-with-fingerprint [] + (assoc (field-defaults) :last_analyzed true :fingerprint_version true :fingerprint true)) (def ^:private field:movie-id (merge - field-defaults + (field-defaults) {:name "id" :display_name "ID" :database_type "SERIAL" @@ -151,9 +134,9 @@ :database_position 0 :position 0})) -(def ^:private field:movie-studio +(defn- field:movie-studio [] (merge - field-defaults-with-fingerprint + (field-defaults-with-fingerprint) {:name "studio" :display_name "Studio" :database_type "VARCHAR" @@ -163,9 +146,9 @@ :database_position 2 :position 2})) -(def ^:private field:movie-title +(defn- field:movie-title [] (merge - field-defaults-with-fingerprint + (field-defaults-with-fingerprint) {:name "title" :display_name "Title" :database_type "VARCHAR" @@ -174,9 +157,9 @@ :database_position 1 :position 1})) -(def ^:private field:studio-name +(defn- field:studio-name [] (merge - field-defaults-with-fingerprint + (field-defaults-with-fingerprint) {:name "name" :display_name "Name" :database_type "VARCHAR" @@ -186,9 +169,9 @@ :position 1})) ;; `studio.studio`? huh? -(def ^:private field:studio-studio +(defn- field:studio-studio [] (merge - field-defaults + (field-defaults) {:name "studio" :display_name "Studio" :database_type "VARCHAR" @@ -202,29 +185,34 @@ (sync/sync-database! db) (sync/sync-database! db) (let [[movie studio] (mapv table-details (db/select Table :db_id (u/get-id db) {:order-by [:name]}))] - (is (= (merge table-defaults {:schema "default" - :name "movie" - :display_name "Movie" - :fields [field:movie-id field:movie-studio field:movie-title]}) - movie)) - (is (= (merge table-defaults {:name "studio" - :display_name "Studio" - :fields [field:studio-name field:studio-studio]}) - studio))))) - + (testing "`movie` Table" + (is (= (merge + (table-defaults) + {:schema "default" + :name "movie" + :display_name "Movie" + :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)]}) + studio)))))) (deftest sync-table-test (mt/with-temp* [Database [db {:engine :metabase.sync-database-test/sync-test}] Table [table {:name "movie", :schema "default", :db_id (u/get-id db)}]] (sync/sync-table! table) (is (= (merge - table-defaults + (table-defaults) {:schema "default" :name "movie" :display_name "Movie" :fields [field:movie-id - (assoc field:movie-studio :fk_target_field_id false :special_type nil) - field:movie-title]}) + (assoc (field:movie-studio) :fk_target_field_id false :special_type nil) + (field:movie-title)]}) (table-details (Table (:id table)))))))