From 2911c4b95c7ee5a66a03d07029e7c9144e502f05 Mon Sep 17 00:00:00 2001
From: Noah Moss <32746338+noahmoss@users.noreply.github.com>
Date: Wed, 26 Jan 2022 17:13:14 -0500
Subject: [PATCH] Fix a couple of edge cases in column ordering for exports
 (#19882)

Co-authored-by: Nemanja <31325167+nemanjaglumac@users.noreply.github.com>
---
 ...ative-query-export-column-order.cy.spec.js | 83 +++++++++++++++++++
 .../shared/models/visualization_settings.cljc |  9 +-
 src/metabase/query_processor/streaming.clj    | 13 +--
 .../query_processor/streaming_test.clj        |  7 +-
 4 files changed, 101 insertions(+), 11 deletions(-)
 create mode 100644 frontend/test/metabase/scenarios/downloads/reproductions/19889-native-query-export-column-order.cy.spec.js

diff --git a/frontend/test/metabase/scenarios/downloads/reproductions/19889-native-query-export-column-order.cy.spec.js b/frontend/test/metabase/scenarios/downloads/reproductions/19889-native-query-export-column-order.cy.spec.js
new file mode 100644
index 00000000000..35214d20ff3
--- /dev/null
+++ b/frontend/test/metabase/scenarios/downloads/reproductions/19889-native-query-export-column-order.cy.spec.js
@@ -0,0 +1,83 @@
+import { restore, downloadAndAssert } from "__support__/e2e/cypress";
+
+const questionDetails = {
+  name: "19889",
+  native: {
+    query: 'select 1 "column a", 2 "column b", 3 "column c"',
+  },
+};
+
+const testCases = ["csv", "xlsx"];
+
+describe("issue 19889", () => {
+  beforeEach(() => {
+    cy.intercept("POST", "/api/dataset").as("dataset");
+
+    restore();
+    cy.signInAsAdmin();
+
+    cy.createNativeQuestion(questionDetails).then(({ body: { id } }) => {
+      cy.wrap(id).as("questionId");
+
+      cy.intercept("POST", `/api/card/${id}/query`).as("cardQuery");
+      cy.visit(`/question/${id}`);
+
+      cy.wait("@cardQuery");
+
+      // Reorder columns a and b
+      cy.findByText("column a")
+        .trigger("mousedown", 0, 0, { force: true })
+        .trigger("mousemove", 5, 5, { force: true })
+        .trigger("mousemove", 100, 0, { force: true })
+        .trigger("mouseup", 100, 0, { force: true });
+      cy.findByText("Started from").click(); // Give DOM some time to update
+    });
+  });
+
+  testCases.forEach(fileType => {
+    it(`should order columns correctly in unsaved native query exports`, () => {
+      downloadAndAssert({ fileType, raw: true }, sheet => {
+        expect(sheet["A1"].v).to.equal("column b");
+        expect(sheet["B1"].v).to.equal("column a");
+        expect(sheet["C1"].v).to.equal("column c");
+      });
+    });
+
+    it(`should order columns correctly in saved native query exports`, () => {
+      saveAndOverwrite();
+
+      cy.get("@questionId").then(questionId => {
+        downloadAndAssert({ fileType, questionId, raw: true }, sheet => {
+          expect(sheet["A1"].v).to.equal("column b");
+          expect(sheet["B1"].v).to.equal("column a");
+          expect(sheet["C1"].v).to.equal("column c");
+        });
+      });
+    });
+
+    it(`should order columns correctly in saved native query exports when the query was modified but not re-run before save (#19889)`, () => {
+      cy.contains(/open editor/i).click();
+      cy.get(".ace_editor").type(
+        '{selectall}select 1 "column x", 2 "column y", 3 "column c"',
+      );
+
+      saveAndOverwrite();
+
+      cy.get("@questionId").then(questionId => {
+        cy.visit(`/question/${questionId}`);
+        cy.wait("@cardQuery");
+
+        downloadAndAssert({ fileType, questionId, raw: true }, sheet => {
+          expect(sheet["A1"].v).to.equal("column x");
+          expect(sheet["B1"].v).to.equal("column y");
+          expect(sheet["C1"].v).to.equal("column c");
+        });
+      });
+    });
+  });
+});
+
+function saveAndOverwrite() {
+  cy.findByText("Save").click();
+  cy.button("Save").click();
+}
diff --git a/shared/src/metabase/shared/models/visualization_settings.cljc b/shared/src/metabase/shared/models/visualization_settings.cljc
index df3afe50624..a1e64ea6337 100644
--- a/shared/src/metabase/shared/models/visualization_settings.cljc
+++ b/shared/src/metabase/shared/models/visualization_settings.cljc
@@ -420,11 +420,12 @@
   (set/map-invert db->norm-param-ref-keys))
 
 (def ^:private db->norm-table-columns-keys
-  {:name     ::table-column-name
+  {:name      ::table-column-name
    ; for now, do not translate the value of this key (the field vector)
-   :fieldref ::table-column-field-ref
-   :fieldRef ::table-column-field-ref
-   :enabled  ::table-column-enabled})
+   :fieldref  ::table-column-field-ref
+   :field_ref ::table-column-field-ref
+   :fieldRef  ::table-column-field-ref
+   :enabled   ::table-column-enabled})
 
 (def ^:private norm->db-table-columns-keys
   (set/map-invert db->norm-table-columns-keys))
diff --git a/src/metabase/query_processor/streaming.clj b/src/metabase/query_processor/streaming.clj
index 8ed184b00b9..e8cfe536c44 100644
--- a/src/metabase/query_processor/streaming.clj
+++ b/src/metabase/query_processor/streaming.clj
@@ -35,14 +35,15 @@
        (mbql.u/uniquify-names (map :name cols))))
 
 (defn- validate-table-columms
-  "Validate that all of the field refs in `table-columns` correspond to actual columns in `cols`, if `cols` contains
-  field refs. Returns `nil` if any do not, so that we fall back to using `cols` directly for the export (#19465).
+  "Validate that all of the columns in `table-columns` correspond to actual columns in `cols`, correlating them by
+  field ref or name. Returns `nil` if any do not, so that we fall back to using `cols` directly for the export (#19465).
   Otherwise returns `table-columns`."
   [table-columns cols]
-  (let [col-field-refs (set (remove nil? (map :field_ref cols)))]
-    ;; If there are no field refs in `cols` (e.g. for native queries), we should use `table-columns` as-is
-    (when (or (empty? col-field-refs)
-              (every? (fn [table-col] (col-field-refs (::mb.viz/table-column-field-ref table-col))) table-columns))
+  (let [col-field-refs (set (remove nil? (map :field_ref cols)))
+        col-names      (set (remove nil? (map :name cols)))]
+    (when (every? (fn [table-col] (or (col-field-refs (::mb.viz/table-column-field-ref table-col))
+                                      (col-names (::mb.viz/table-column-name table-col))))
+                  table-columns)
       table-columns)))
 
 (defn- export-column-order
diff --git a/test/metabase/query_processor/streaming_test.clj b/test/metabase/query_processor/streaming_test.clj
index 2d6ed8677c2..1f8e6b3a654 100644
--- a/test/metabase/query_processor/streaming_test.clj
+++ b/test/metabase/query_processor/streaming_test.clj
@@ -476,7 +476,12 @@
            (@#'qp.streaming/export-column-order
             [{:id 0, :name "Col1" :field_ref [:field 0 nil]}]
             [{::mb.viz/table-column-field-ref [:field 1 nil], ::mb.viz/table-column-enabled true}
-             {::mb.viz/table-column-field-ref [:field 2 nil], ::mb.viz/table-column-enabled true}]))))
+             {::mb.viz/table-column-field-ref [:field 2 nil], ::mb.viz/table-column-enabled true}])))
+    (is (= [0]
+           (@#'qp.streaming/export-column-order
+            [{:id 0, :name "Col1" :field_ref [:field 0 nil]}]
+            [{::mb.viz/table-column-name "Col1" , ::mb.viz/table-column-enabled true}
+             {::mb.viz/table-column-name "Col2" , ::mb.viz/table-column-enabled true}]))))
 
   (testing "if table-columns is nil, original order of cols is used"
     (is (= [0 1]
-- 
GitLab