From 885905ebe5d0c912e63b6376464f3f35be3aedd5 Mon Sep 17 00:00:00 2001
From: github-automation-metabase
 <166700802+github-automation-metabase@users.noreply.github.com>
Date: Tue, 26 Nov 2024 17:57:09 -0500
Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20backported=20"remove=20pivot-gro?=
 =?UTF-8?q?uping=20row=20and=20columns=20in=20tandem"=20(#50530)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* remove pivot-grouping row and columns in tandem (#50476)

* remove pivot-grouping row and columns in tandem

* add pivot card scalar test for csvs and xlsx.

* improve test feedback

* add a 'todo' for pivoted json exports

* indent

* tap tap tap

---------

Co-authored-by: bryan <bryan.maass@gmail.com>
Co-authored-by: Adam James <adam.vermeer2@gmail.com>
---
 .../query_processor/streaming/xlsx.clj        | 40 +++++++++-------
 test/metabase/api/downloads_exports_test.clj  | 46 +++++++++++++++++++
 2 files changed, 70 insertions(+), 16 deletions(-)

diff --git a/src/metabase/query_processor/streaming/xlsx.clj b/src/metabase/query_processor/streaming/xlsx.clj
index 7d2ef31ac93..7f9bc06418a 100644
--- a/src/metabase/query_processor/streaming/xlsx.clj
+++ b/src/metabase/query_processor/streaming/xlsx.clj
@@ -440,12 +440,14 @@
 (defmulti ^:private add-row!
   "Adds a row of values to the spreadsheet. Values with the `scaled` viz setting are scaled prior to being added.
 
-  This is based on the equivalent function in Docjure, but adapted to support Metabase viz settings."
+  This is based on the equivalent function in Docjure: [[spreadsheet/add-row!]], but adapted to support Metabase viz
+  settings."
   {:arglists '([sheet values cols col-settings cell-styles typed-cell-styles]
                [sheet row-num values cols col-settings cell-styles typed-cell-styles])}
   (fn [sheet & _args]
     (class sheet)))
 
+;; TODO this add-row! and the one below (For XSSFSheet) should be consolidated
 (defmethod add-row! org.apache.poi.xssf.streaming.SXSSFSheet
   ([^SXSSFSheet sheet values cols col-settings cell-styles typed-cell-styles]
    (let [row-num (if (= 0 (.getPhysicalNumberOfRows sheet))
@@ -466,7 +468,8 @@
                id-or-name   (or (:id col) (:name col))
                settings     (or (get col-settings {::mb.viz/field-id id-or-name})
                                 (get col-settings {::mb.viz/column-name id-or-name}))
-               scaled-val   (if (and value (::mb.viz/scale settings))
+               ;; value can be a column header (a string), so if the column is scaled, it'll try to do (* "count" 7)
+               scaled-val   (if (and (number? value) (::mb.viz/scale settings))
                               (* value (::mb.viz/scale settings))
                               value)
                ;; Temporal values are converted into strings in the format-rows QP middleware, which is enabled during
@@ -499,7 +502,8 @@
                id-or-name   (or (:id col) (:name col))
                settings     (or (get col-settings {::mb.viz/field-id id-or-name})
                                 (get col-settings {::mb.viz/column-name id-or-name}))
-               scaled-val   (if (and value (::mb.viz/scale settings))
+               ;; value can be a column header (a string), so if the column is scaled, it'll try to do (* "count" 7)
+               scaled-val   (if (and (number? value) (::mb.viz/scale settings))
                               (* value (::mb.viz/scale settings))
                               value)
                ;; Temporal values are converted into strings in the format-rows QP middleware, which is enabled during
@@ -618,7 +622,7 @@
       (.addColumnLabel pivot-table DataConsolidateFunction/SUM #_(get aggregation-functions idx DataConsolidateFunction/SUM) idx))
     (doseq [[idx sort-setting] column-sort-order]
       (let [setting (case sort-setting
-                      :ascending STFieldSortType/ASCENDING
+                      :ascending  STFieldSortType/ASCENDING
                       :descending STFieldSortType/DESCENDING)]
         (when setting
           (-> pivot-table
@@ -698,26 +702,30 @@
             (vreset! typed-cell-styles (compute-typed-cell-styles workbook data-format)))))
 
       (write-row! [_ row row-num ordered-cols {:keys [output-order] :as viz-settings}]
-        (let [ordered-row        (vec (if output-order
-                                        (let [row-v (into [] row)]
-                                          (for [i output-order] (row-v i)))
-                                        row))
-              col-settings       (::mb.viz/column-settings viz-settings)
-              pivot-grouping-key @pivot-grouping-idx
-              group              (get row pivot-grouping-key)
-              modified-row       (cond->> ordered-row
-                                   pivot-grouping-key (m/remove-nth pivot-grouping-key))
-              {:keys [sheet]}    @workbook-data]
+        (let [ordered-row          (vec (if output-order
+                                          (let [row-v (into [] row)]
+                                            (for [i output-order] (row-v i)))
+                                          row))
+              col-settings         (::mb.viz/column-settings viz-settings)
+              pivot-grouping-key   @pivot-grouping-idx
+              group                (get row pivot-grouping-key)
+              [row' ordered-cols'] (cond->> [ordered-row ordered-cols]
+                                     pivot-grouping-key
+                                     ;; We need to remove the pivot-grouping key if it's there, because we don't show
+                                     ;; it in the export. `ordered-cols` is a parallel array, so we must remove the
+                                     ;; corresponding col.
+                                     (map #(m/remove-nth pivot-grouping-key %)))
+              {:keys [sheet]}      @workbook-data]
           (when (or (not group)
                     (= qp.pivot.postprocess/NON_PIVOT_ROW_GROUP (int group)))
-            (add-row! sheet (inc row-num) modified-row ordered-cols col-settings @cell-styles @typed-cell-styles)
+            (add-row! sheet (inc row-num) row' ordered-cols' col-settings @cell-styles @typed-cell-styles)
             (when (= (inc row-num) *auto-sizing-threshold*)
               (autosize-columns! sheet)))))
 
       (finish! [_ {:keys [row_count]}]
         (let [{:keys [workbook sheet]} @workbook-data]
           (when (or (nil? row_count) (< row_count *auto-sizing-threshold*))
-                ;; Auto-size columns if we never hit the row threshold, or a final row count was not provided
+            ;; Auto-size columns if we never hit the row threshold, or a final row count was not provided
             (autosize-columns! sheet))
           (try
             (spreadsheet/save-workbook-into-stream! os workbook)
diff --git a/test/metabase/api/downloads_exports_test.clj b/test/metabase/api/downloads_exports_test.clj
index 00ed156a14f..16d1771fdd5 100644
--- a/test/metabase/api/downloads_exports_test.clj
+++ b/test/metabase/api/downloads_exports_test.clj
@@ -12,6 +12,7 @@
   #_{:clj-kondo/ignore [:deprecated-namespace]}
   (:require
    [cheshire.core :as json]
+   [clojure.data :as data]
    [clojure.data.csv :as csv]
    [clojure.java.io :as io]
    [clojure.set :as set]
@@ -1339,3 +1340,48 @@
                     :dashcard-download        expected-header
                     :public-dashcard-download expected-header}
                    (update-vals formatted-results first)))))))))
+
+(defn- pivot-card-with-scalar [scalar]
+  {:display                :pivot
+   :visualization_settings {:pivot_table.column_split
+                            {:rows    ["CATEGORY"]
+                             :columns ["CREATED_AT"]
+                             :values  ["sum"]}
+                            :column_settings
+                            {"[\"name\",\"sum\"]" (merge {:number_style       "currency"
+                                                          :currency_in_header false}
+                                                         (when scalar {:scale scalar}))}}
+   :dataset_query          {:database (mt/id)
+                            :type     :query
+                            :query {:source-table (mt/id :products)
+                                    :aggregation  [[:sum [:field (mt/id :products :price) {:base-type :type/Float}]]]
+                                    :breakout     [[:field (mt/id :products :category) {:base-type :type/Text}]
+                                                   [:field (mt/id :products :created_at) {:base-type :type/DateTime :temporal-unit :year}]]}}})
+
+(deftest pivot-with-scale-test
+  (testing "Pivot table exports work with \"Multiply by a number\" (scale)"
+    (mt/dataset test-data
+      (mt/with-temp [:model/Card {:keys [created_at] :as no-scale-card}  (pivot-card-with-scalar nil)
+                     :model/Card one-scale-card (assoc (pivot-card-with-scalar 1) :created_at created_at)
+                     :model/Card zero-scale-card (assoc (pivot-card-with-scalar 0) :created_at created_at)]
+        (let [named-cards {:one-scale-card one-scale-card
+                           :two-scale-card zero-scale-card
+                           :no-scale-card no-scale-card}]
+          (doseq [[c1-name c2-name export-format expected] [[:one-scale-card  :no-scale-card  :csv  true]
+                                                            [:one-scale-card  :two-scale-card :csv  false]
+                                                            [:no-scale-card   :two-scale-card :csv  false]
+                                                            [:one-scale-card  :no-scale-card  :xlsx true]
+                                                            [:one-scale-card  :two-scale-card :xlsx false]
+                                                            [:no-scale-card   :two-scale-card :xlsx false]
+                                                            ;; TODO: We don't support JSON for pivot tables, once we
+                                                            ;; do, we should add them here
+                                                            ]]
+            (testing (str "> " (name c1-name) " and " (name c2-name) " with export-format: '" (name export-format) "' should be " expected)
+              (let [c1 (get named-cards c1-name)
+                    c2 (get named-cards c2-name)
+                    [unique-to-a unique-to-b _both]
+                    (data/diff (all-outputs! c1 {:export-format export-format :format-rows true :pivot true})
+                               (all-outputs! c2 {:export-format export-format :format-rows true :pivot true}))]
+                (if expected
+                  (is (= [nil nil] [unique-to-a unique-to-b]))
+                  (is (or (some? unique-to-a) (some? unique-to-b))))))))))))
-- 
GitLab