(ns metabase.models.table (:require [metabase.api.common :as api] [metabase.audit :as audit] [metabase.db.query :as mdb.query] [metabase.driver :as driver] [metabase.models.audit-log :as audit-log] [metabase.models.humanization :as humanization] [metabase.models.interface :as mi] [metabase.models.serialization :as serdes] [metabase.permissions.core :as perms] [metabase.premium-features.core :refer [defenterprise]] [metabase.search.core :as search] [metabase.util :as u] [methodical.core :as methodical] [toucan2.core :as t2])) | |
----------------------------------------------- Constants + Entity ----------------------------------------------- | |
Valid values for | (def visibility-types #{:hidden :technical :cruft}) |
Valid values for | (def field-orderings #{:database :alphabetical :custom :smart}) |
--------------------------------------------------- Lifecycle ---------------------------------------------------- | |
(methodical/defmethod t2/table-name :model/Table [_model] :metabase_table) | |
(doto :model/Table (derive :metabase/model) (derive ::mi/read-policy.full-perms-for-perms-set) (derive ::mi/write-policy.full-perms-for-perms-set) (derive :hook/timestamped?) ;; Deliberately **not** deriving from `:hook/entity-id` because we should not be randomizing the `entity_id`s on ;; databases, tables or fields. Since the sync process can create them in multiple instances, randomizing them would ;; cause duplication rather than good matching if the two instances are later linked by serdes. #_(derive :hook/entity-id)) | |
(t2/deftransforms :model/Table {:entity_type mi/transform-keyword :visibility_type mi/transform-keyword :field_order mi/transform-keyword}) | |
(methodical/defmethod t2/model-for-automagic-hydration [:default :table] [_original-model _k] :model/Table) | |
(t2/define-before-insert :model/Table [table] (let [defaults {:display_name (humanization/name->human-readable-name (:name table)) :field_order (driver/default-field-order (t2/select-one-fn :engine :model/Database :id (:db_id table)))}] (merge defaults table))) | |
(t2/define-before-delete :model/Table [table] ;; We need to use toucan to delete the fields instead of cascading deletes because MySQL doesn't support columns with cascade delete ;; foreign key constraints in generated columns. #44866 (t2/delete! :model/Field :table_id (:id table))) | |
(defn- set-new-table-permissions! [table] (t2/with-transaction [_conn] (let [all-users-group (perms/all-users-group) non-magic-groups (perms/non-magic-groups) non-admin-groups (conj non-magic-groups all-users-group)] ;; Data access permissions (if (= (:db_id table) audit/audit-db-id) (do ;; Tables in audit DB should start out with no query access in all groups (perms/set-new-table-permissions! non-admin-groups table :perms/view-data :unrestricted) (perms/set-new-table-permissions! non-admin-groups table :perms/create-queries :no)) (do ;; Normal tables start out with unrestricted data access in all groups, but query access only in All Users (perms/set-new-table-permissions! non-admin-groups table :perms/view-data :unrestricted) (perms/set-new-table-permissions! [all-users-group] table :perms/create-queries :query-builder) (perms/set-new-table-permissions! non-magic-groups table :perms/create-queries :no))) ;; Download permissions (perms/set-new-table-permissions! [all-users-group] table :perms/download-results :one-million-rows) (perms/set-new-table-permissions! non-magic-groups table :perms/download-results :no) ;; Table metadata management (perms/set-new-table-permissions! non-admin-groups table :perms/manage-table-metadata :no)))) | |
(t2/define-after-insert :model/Table [table] (u/prog1 table (set-new-table-permissions! table))) | |
(defmethod mi/can-read? :model/Table ([instance] (and (perms/user-has-permission-for-table? api/*current-user-id* :perms/view-data :unrestricted (:db_id instance) (:id instance)) (perms/user-has-permission-for-table? api/*current-user-id* :perms/create-queries :query-builder (:db_id instance) (:id instance)))) ([_ pk] (mi/can-read? (t2/select-one :model/Table pk)))) | |
OSS implementation. Returns a boolean whether the current user can write the given field. | (defenterprise current-user-can-write-table? metabase-enterprise.advanced-permissions.common [_instance] (mi/superuser?)) |
(defmethod mi/can-write? :model/Table ([instance] (current-user-can-write-table? instance)) ([_ pk] (mi/can-write? (t2/select-one :model/Table pk)))) | |
(defmethod serdes/hash-fields :model/Table [_table] [:schema :name (serdes/hydrated-hash :db :db_id)]) | |
------------------------------------------------ Field ordering ------------------------------------------------- | |
How should we order fields. | (def field-order-rule [[:position :asc] [:%lower.name :asc]]) |
Update | (defn update-field-positions! [table] (doall (map-indexed (fn [new-position field] (t2/update! :model/Field (u/the-id field) {:position new-position})) ;; Can't use `select-field` as that returns a set while we need an ordered list (t2/select [:model/Field :id] :table_id (u/the-id table) {:order-by (case (:field_order table) :custom [[:custom_position :asc]] :smart [[[:case (mdb.query/isa :semantic_type :type/PK) 0 (mdb.query/isa :semantic_type :type/Name) 1 (mdb.query/isa :semantic_type :type/Temporal) 2 :else 3] :asc] [:%lower.name :asc]] :database [[:database_position :asc]] :alphabetical [[:%lower.name :asc]])})))) |
Field ordering is valid if all the fields from a given table are present and only from that table. | (defn- valid-field-order? [table field-ordering] (= (t2/select-pks-set :model/Field :table_id (u/the-id table) :active true) (set field-ordering))) |
Set field order to | (defn custom-order-fields! [table field-order] {:pre [(valid-field-order? table field-order)]} (t2/with-transaction [_] (t2/update! :model/Table (u/the-id table) {:field_order :custom}) (dorun (map-indexed (fn [position field-id] (t2/update! :model/Field field-id {:position position :custom_position position})) field-order)))) |
--------------------------------------------------- Hydration ---------------------------------------------------- | |
(methodical/defmethod t2/batched-hydrate [:model/Table :field_values] "Return the FieldValues for all Fields belonging to a single `table`." [_model k tables] (mi/instances-with-hydrated-data tables k #(-> (group-by :table_id (t2/select [:model/FieldValues :field_id :values :field.table_id] {:join [[:metabase_field :field] [:= :metabase_fieldvalues.field_id :field.id]] :where [:and [:in :field.table_id [(map :id tables)]] [:= :field.visibility_type "normal"] [:= :metabase_fieldvalues.type "full"]]})) (update-vals (fn [fvs] (->> fvs (map (juxt :field_id :values)) (into {}))))) :id)) | |
(methodical/defmethod t2/batched-hydrate [:model/Table :pk_field] [_model k tables] (mi/instances-with-hydrated-data tables k #(t2/select-fn->fn :table_id :id :model/Field :table_id [:in (map :id tables)] :semantic_type (mdb.query/isa :type/PK) :visibility_type [:not-in ["sensitive" "retired"]]) :id)) | |
(defn- with-objects [hydration-key fetch-objects-fn tables] (let [table-ids (set (map :id tables)) table-id->objects (group-by :table_id (when (seq table-ids) (fetch-objects-fn table-ids)))] (for [table tables] (assoc table hydration-key (get table-id->objects (:id table) []))))) | |
(mi/define-batched-hydration-method with-segments :segments "Efficiently hydrate the Segments for a collection of `tables`." [tables] (with-objects :segments (fn [table-ids] (t2/select :model/Segment :table_id [:in table-ids], :archived false, {:order-by [[:name :asc]]})) tables)) | |
(mi/define-batched-hydration-method with-metrics :metrics "Efficiently hydrate the Metrics for a collection of `tables`." [tables] (with-objects :metrics (fn [table-ids] (->> (t2/select :model/Card :table_id [:in table-ids], :archived false, :type :metric, {:order-by [[:name :asc]]}) (filter mi/can-read?))) tables)) | |
Efficiently hydrate the Fields for a collection of | (defn with-fields [tables] (with-objects :fields (fn [table-ids] (t2/select :model/Field :active true :table_id [:in table-ids] :visibility_type [:not= "retired"] {:order-by field-order-rule})) tables)) |
(mi/define-batched-hydration-method fields :fields "Efficiently hydrate the Fields for a collection of `tables`" [tables] (with-fields tables)) | |
------------------------------------------------ Convenience Fns ------------------------------------------------- | |
Return the | (defn database [table] (t2/select-one :model/Database :id (:db_id table))) |
------------------------------------------------- Serialization ------------------------------------------------- | (defmethod serdes/dependencies "Table" [table] [[{:model "Database" :id (:db_id table)}]]) |
(defmethod serdes/generate-path "Table" [_ table] (let [db-name (t2/select-one-fn :name :model/Database :id (:db_id table))] (filterv some? [{:model "Database" :id db-name} (when (:schema table) {:model "Schema" :id (:schema table)}) {:model "Table" :id (:name table)}]))) | |
(defmethod serdes/entity-id "Table" [_ {:keys [name]}] name) | |
(defmethod serdes/load-find-local "Table" [path] (let [db-name (-> path first :id) schema-name (when (= 3 (count path)) (-> path second :id)) table-name (-> path last :id) db-id (t2/select-one-pk :model/Database :name db-name)] (t2/select-one :model/Table :name table-name :db_id db-id :schema schema-name))) | |
(defmethod serdes/make-spec "Table" [_model-name _opts] {:copy [:name :description :entity_type :active :display_name :visibility_type :schema :points_of_interest :caveats :show_in_getting_started :field_order :initial_sync_status :is_upload :database_require_filter :entity_id] :skip [:estimated_row_count :view_count] :transform {:created_at (serdes/date) :db_id (serdes/fk :model/Database :name)}}) | |
(defmethod serdes/storage-path "Table" [table _ctx] (concat (serdes/storage-path-prefixes (serdes/path table)) [(:name table)])) | |
-------------------------------------------------- Audit Log Table ------------------------------------------------- | |
(defmethod audit-log/model-details :model/Table [table _event-type] (select-keys table [:id :name :db_id])) | |
------------------------------------------------- Search ---------------------------------------------------------- | |
(search/define-spec "table" {:model :model/Table :attrs {;; legacy search uses :active for this, but then has a rule to only ever show active tables ;; so we moved that to the where clause :archived false :collection-id false :creator-id false :database-id :db_id :view-count true :created-at true :updated-at true} :search-terms [:name :display_name :description] :render-terms {:initial-sync-status true :table-id :id :table-description :description :table-name :name :table-schema :schema :database-name :db.name} :where [:and :active [:= :visibility_type nil] [:not= :db_id [:inline audit/audit-db-id]]] :joins {:db [:model/Database [:= :db.id :this.db_id]]}}) | |