(ns metabase.api.query-metadata (:require [metabase.api.field :as api.field] [metabase.api.table :as api.table] [metabase.legacy-mbql.normalize :as mbql.normalize] [metabase.lib.schema.id :as lib.schema.id] [metabase.lib.util :as lib.util] [metabase.models.interface :as mi] [metabase.util :as u] [metabase.util.malli :as mu] [toucan2.core :as t2])) | |
(defn- get-databases [ids] (when (seq ids) (into [] (filter mi/can-read?) (t2/select :model/Database :id [:in ids])))) | |
(defn- field-ids->table-ids [field-ids] (if (seq field-ids) (t2/select-fn-set :table_id :model/Field :id [:in field-ids]) #{})) | |
(defn- split-tables-and-legacy-card-refs [source-ids] (-> (reduce (fn [m src] (if-let [card-id (lib.util/legacy-string-table-id->card-id src)] (update m :cards conj! card-id) (update m :tables conj! src))) {:cards (transient #{}) :tables (transient #{})} source-ids) (update-vals persistent!))) | |
(defn- query->template-tag-field-ids [query] (when-let [template-tags (some-> query :native :template-tags vals seq)] (for [{tag-type :type, [dim-tag id _opts] :dimension} template-tags :when (and (= tag-type :dimension) (= dim-tag :field) (integer? id))] id))) | |
Fetch dependent metadata for ad-hoc queries. | (defn- batch-fetch-query-metadata* [queries] (let [source-ids (into #{} (mapcat #(lib.util/collect-source-tables (:query %))) queries) {source-table-ids :tables source-card-ids :cards} (split-tables-and-legacy-card-refs source-ids) source-tables (concat (api.table/batch-fetch-table-query-metadatas source-table-ids) (api.table/batch-fetch-card-query-metadatas source-card-ids)) fk-target-field-ids (into #{} (comp (mapcat :fields) (keep :fk_target_field_id)) source-tables) fk-target-table-ids (into #{} (remove source-table-ids) (field-ids->table-ids fk-target-field-ids)) fk-target-tables (api.table/batch-fetch-table-query-metadatas fk-target-table-ids) tables (concat source-tables fk-target-tables) template-tag-field-ids (into #{} (mapcat query->template-tag-field-ids) queries) query-database-ids (into #{} (keep :database) queries) database-ids (into query-database-ids (keep :db_id) tables)] {;; TODO: This is naive and issues multiple queries currently. That's probably okay for most dashboards, ;; since they tend to query only a handful of databases at most. :databases (sort-by :id (get-databases database-ids)) ;; apparently some of these tables come back with normal integer IDs and some come back with string IDs... not sure ;; what the hecc is going on but I guess maybe some of them in 2024 are still using that old `card__<id>` hack or ;; something. Idk. Anyways let's just sort the ones with numeric IDs first and the ones with string IDs last. We're ;; sorting these in a sane order because lots of tests expect them to be sorted by numeric ID and break if you sort ;; by something like `(str (:id %))`. -- Cam :tables (sort-by (fn [{:keys [id]}] (if (integer? id) [id ""] [Integer/MAX_VALUE (str id)])) tables) :fields (sort-by :id (api.field/get-fields template-tag-field-ids))})) |
Fetch dependent metadata for ad-hoc queries. | (defn batch-fetch-query-metadata [queries] (batch-fetch-query-metadata* (map mbql.normalize/normalize queries))) |
Fetch dependent metadata for cards. Models and native queries need their definitions walked as well as their own, card-level metadata. | (defn batch-fetch-card-metadata [cards] (let [queries (into (vec (keep :dataset_query cards)) ; All the queries on all the cards ;; Plus the card-level metadata of each model and native query (comp (filter (fn [card] (or (= :model (:type card)) (= :native (-> card :dataset_query :type))))) (map (fn [card] {:query {:source-table (str "card__" (u/the-id card))}}))) cards)] (batch-fetch-query-metadata queries))) |
(defn- click-behavior->link-details [{:keys [linkType type targetId] :as _click-behavior}] (when (= type "link") (when-let [link-type (case linkType "question" :card "dashboard" :dashboard nil)] {:type link-type :id targetId}))) | |
(mu/defn- get-cards :- [:maybe [:sequential [:map [:id ::lib.schema.id/card]]]] [ids :- [:maybe [:or [:sequential ::lib.schema.id/card] [:set ::lib.schema.id/card]]]] (when (seq ids) (let [cards (into [] (filter mi/can-read?) (t2/select :model/Card :id [:in ids]))] (t2/hydrate cards :can_write)))) | |
(defn- dashcard->click-behaviors [dashcard] (let [viz-settings (:visualization_settings dashcard) top-click-behavior (:click_behavior viz-settings) col-click-behaviors (keep (comp :click_behavior val) (:column_settings viz-settings))] (conj col-click-behaviors top-click-behavior))) | |
(defn- batch-fetch-linked-dashboards [dashboard-ids] (when (seq dashboard-ids) (let [dashboards (->> (t2/select :model/Dashboard :id [:in dashboard-ids]) (filter mi/can-read?))] (t2/hydrate dashboards :can_write :param_fields)))) | |
(defn- batch-fetch-dashboard-links [dashcards] (let [links (group-by :type (into #{} (comp (mapcat dashcard->click-behaviors) (keep click-behavior->link-details)) dashcards)) link-cards (get-cards (into #{} (map :id) (:card links))) dashboards (->> (:dashboard links) (into #{} (map :id)) batch-fetch-linked-dashboards)] {:cards (sort-by :id link-cards) :dashboards (sort-by :id dashboards)})) | |
Fetch dependent metadata for dashboards. | (defn batch-fetch-dashboard-metadata [dashboards] (let [dashcards (mapcat :dashcards dashboards) cards (for [{:keys [card series]} dashcards :let [all (conj series card)] card all] card) card-ids (into #{} (map :id) cards) links (batch-fetch-dashboard-links dashcards)] (merge (->> (remove (comp card-ids :id) (:cards links)) (concat cards) batch-fetch-card-metadata) {:cards (or (:cards links) []) :dashboards (or (:dashboards links) [])}))) |