Object details drill for FK fields. Creates a new query based on the FK table with a = filter based on selected value.

Entry points:

  • Cell

Requirements:

  • Column is type/FK
  • value is not null

Query transformation:

  • Find the target table for the selected FK

  • Create a new query that targets the FK table and add a = filter using the corresponding PK column and the selected value

  • Preserve filters from the original query that use = operator and are based on other FK columns targeting the same FK table as the selected FK. This can only happen when the target table's PK consists of multiple columns.

Question transformation:

  • Set default display

An FK details drill is one where you click a foreign key value in a table view e.g. ORDERS.USER_ID and choose the 'View details' option, then it shows you the PEOPLE record in question (e.g. Person 5 if USER_ID was 5).

We will only possibly return one of the 'object details' drills ([[metabase.lib.drill-thru.pk]], [[metabase.lib.drill-thru.fk-details]], or [[metabase.lib.drill-thru.zoom]]); see [[metabase.lib.drill-thru.object-details]] for the high-level logic that calls out to the individual implementations.

(ns metabase.lib.drill-thru.fk-details
  (:require
   [metabase.lib.drill-thru.common :as lib.drill-thru.common]
   [metabase.lib.filter :as lib.filter]
   [metabase.lib.metadata :as lib.metadata]
   [metabase.lib.query :as lib.query]
   [metabase.lib.schema :as lib.schema]
   [metabase.lib.schema.drill-thru :as lib.schema.drill-thru]
   [metabase.lib.types.isa :as lib.types.isa]
   [metabase.util.malli :as mu]))
(mu/defn fk-details-drill :- [:maybe ::lib.schema.drill-thru/drill-thru.fk-details]
  "Return an `:fk-details` 'View details' drill when clicking on the value of a FK column."
  [query                               :- ::lib.schema/query
   _stage-number                       :- :int
   {:keys [column value] :as _context} :- ::lib.schema.drill-thru/context]
  (when (and (lib.types.isa/foreign-key? column)
             (some? value)
             (not= value :null))
    {:lib/type  :metabase.lib.drill-thru/drill-thru
     :type      :drill-thru/fk-details
     :column    column
     :object-id value
     :many-pks? (lib.drill-thru.common/many-pks? query)}))
(defmethod lib.drill-thru.common/drill-thru-info-method :drill-thru/fk-details
  [_query _stage-number drill-thru]
  (select-keys drill-thru [:many-pks? :object-id :type]))
(defmethod lib.drill-thru.common/drill-thru-method :drill-thru/fk-details
  [query stage-number {:keys [column object-id]} & _]
  ;; generate a NEW query against the FK target table and column, e.g. if the original query was
  ;; ORDERS/ORDERS.USER_ID, the new query should by PEOPLE/PEOPLE.ID.
  ;; If there are filters on the original query which are:
  ;; - := filters, and
  ;; - Not for this same column, but
  ;; - Relevant to OTHER FKs which point to PKs on the target table;
  ;; then preserve those filters.
  (let [fk-column-id     (:fk-target-field-id column)
        fk-column        (some->> fk-column-id (lib.metadata/field query))
        fk-column-table  (some->> (:table-id fk-column) (lib.metadata/table query))
        other-fk-filters (for [filter-clause (lib.filter/filters query stage-number)
                               :let [parts (lib.filter/filter-parts query stage-number filter-clause)]
                               :when (= (:short (:operator parts)) :=)
                               :let [other-fk-target (some->> parts
                                                              :column
                                                              :fk-target-field-id
                                                              (lib.metadata/field query))]
                               :when (and other-fk-target
                                          (= (:table-id other-fk-target) (:id fk-column-table)) ; FK to this table
                                          (not= (:id other-fk-target) fk-column-id))]           ; But not this column
                           (lib.filter/= other-fk-target (first (:args parts))))]
    (reduce #(lib.filter/filter %1 stage-number %2)
            (lib.query/query query fk-column-table)
            (concat other-fk-filters
                    [(lib.filter/= fk-column object-id)]))))