From 80687b8a420b8cd3e58294b8e2cdf96566f99f06 Mon Sep 17 00:00:00 2001
From: Alexander Solovyov <alexander@solovyov.net>
Date: Mon, 27 May 2024 11:19:06 +0300
Subject: [PATCH] [serdes] nested fields should be properly serialized (#43066)

---
 .../serialization/v2/extract_test.clj         | 27 ++++++++++++-------
 .../serialization/v2/load_test.clj            | 24 ++++++++++++-----
 src/metabase/models/field.clj                 |  4 ++-
 3 files changed, 39 insertions(+), 16 deletions(-)

diff --git a/enterprise/backend/test/metabase_enterprise/serialization/v2/extract_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/v2/extract_test.clj
index 461403fd6d8..ada774d8a6c 100644
--- a/enterprise/backend/test/metabase_enterprise/serialization/v2/extract_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/serialization/v2/extract_test.clj
@@ -1658,24 +1658,33 @@
                       (map serdes/path)
                       set)))))))))
 
-(deftest foreign-key-field-test
+(deftest field-references-test
   (mt/with-empty-h2-app-db
-    (ts/with-temp-dpc [Database   {db-id         :id}        {:name "My Database"}
-                       Table      {no-schema-id  :id}        {:name "Schemaless Table" :db_id db-id}
-                       Field      {some-field-id :id}        {:name "Some Field" :table_id no-schema-id}
-                       Table      {schema-id     :id}        {:name        "Schema'd Table"
+    (ts/with-temp-dpc [Database   {db-id          :id}        {:name "My Database"}
+                       Table      {no-schema-id   :id}        {:name "Schemaless Table" :db_id db-id}
+                       Field      {some-field-id  :id}        {:name "Some Field" :table_id no-schema-id}
+                       Table      {schema-id      :id}        {:name        "Schema'd Table"
                                                               :db_id       db-id
                                                               :schema      "PUBLIC"}
-                       Field      _                          {:name "Other Field" :table_id schema-id}
-                       Field      {fk-id         :id}        {:name     "Foreign Key"
+                       Field      {other-field-id :id}        {:name "Other Field" :table_id schema-id}
+                       Field      {fk-id          :id}        {:name     "Foreign Key"
                                                               :table_id schema-id
-                                                              :fk_target_field_id some-field-id}]
+                                                              :fk_target_field_id some-field-id}
+                       Field      {nested-id      :id}        {:name "Nested Field"
+                                                              :table_id schema-id
+                                                              :parent_id other-field-id}]
 
       (testing "fields that reference foreign keys are properly exported as Field references"
         (is (= ["My Database" nil "Schemaless Table" "Some Field"]
                (->> (t2/select-one Field :id fk-id)
                     (serdes/extract-one "Field" {})
-                    :fk_target_field_id)))))))
+                    :fk_target_field_id))))
+
+      (testing "Fields that reference parents are properly exported as Field references"
+        (is (= ["My Database" "PUBLIC" "Schema'd Table" "Other Field"]
+               (->> (t2/select-one Field :id nested-id)
+                    (serdes/extract-one "Field" {})
+                    :parent_id)))))))
 
 (deftest escape-report-test
   (mt/with-empty-h2-app-db
diff --git a/enterprise/backend/test/metabase_enterprise/serialization/v2/load_test.clj b/enterprise/backend/test/metabase_enterprise/serialization/v2/load_test.clj
index d5afe756b73..800452df524 100644
--- a/enterprise/backend/test/metabase_enterprise/serialization/v2/load_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/serialization/v2/load_test.clj
@@ -126,7 +126,8 @@
           t1s        (atom nil)
           t2s        (atom nil)
           f1s        (atom nil)
-          f2s        (atom nil)]
+          f2s        (atom nil)
+          f3s        (atom nil)]
       (ts/with-dbs [source-db dest-db]
         (testing "serializing the two databases"
           (ts/with-db source-db
@@ -136,6 +137,7 @@
             (reset! t2s  (ts/create! Table    :name "posts" :db_id (:id @db2s))) ; Deliberately the same name!
             (reset! f1s  (ts/create! Field    :name "Target Field" :table_id (:id @t1s)))
             (reset! f2s  (ts/create! Field    :name "Foreign Key"  :table_id (:id @t2s) :fk_target_field_id (:id @f1s)))
+            (reset! f3s  (ts/create! Field    :name "Nested Field"   :table_id (:id @t1s) :parent_id (:id @f1s)))
             (reset! serialized (into [] (serdes.extract/extract {})))))
 
         (testing "serialization of databases is based on the :name"
@@ -152,10 +154,16 @@
         (testing "foreign key references are serialized as a field path"
           (is (= ["db1" nil "posts" "Target Field"]
                  (->> @serialized
-                      (filter #(-> % :serdes/meta last :model (= "Field")))
-                      (filter #(-> % :table_id (= ["db2" nil "posts"])))
-                      (keep :fk_target_field_id)
-                      first))))
+                      (u/seek #(and (-> % :serdes/meta last :model (= "Field"))
+                                    (-> % :name (= "Foreign Key"))))
+                      :fk_target_field_id))))
+
+        (testing "Parent field references are serialized as a field path"
+          (is (= ["db1" nil "posts" "Target Field"]
+                 (->> @serialized
+                      (u/seek #(and (-> % :serdes/meta last :model (= "Field"))
+                                    (-> % :name (= "Nested Field"))))
+                      :parent_id))))
 
         (testing "deserialization works properly, keeping the same-named tables apart"
           (ts/with-db dest-db
@@ -170,7 +178,11 @@
             (is (= #{(:id @db1d) (:id @db2d)}
                    (t2/select-fn-set :db_id Table :name "posts")))
             (is (t2/exists? Table :name "posts" :db_id (:id @db1d)))
-            (is (t2/exists? Table :name "posts" :db_id (:id @db2d)))))))))
+            (is (t2/exists? Table :name "posts" :db_id (:id @db2d)))
+            (is (= (t2/select-one-fn :id Field :name (:name @f1s))
+                   (t2/select-one-fn :fk_target_field_id Field :name (:name @f2s))))
+            (is (= (t2/select-one-fn :id Field :name (:name @f1s))
+                   (t2/select-one-fn :parent_id Field :name (:name @f3s))))))))))
 
 (deftest card-dataset-query-test
   ;; Card.dataset_query is a JSON-encoded MBQL query, which contain database, table, and field IDs - these need to be
diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj
index b5d845378bf..d970ce80412 100644
--- a/src/metabase/models/field.clj
+++ b/src/metabase/models/field.clj
@@ -398,13 +398,15 @@
         (update :dimensions         extract-dimensions)
         (update :table_id           serdes/*export-table-fk*)
         (update :fk_target_field_id serdes/*export-field-fk*)
+        (update :parent_id          serdes/*export-field-fk*)
         (dissoc :fingerprint :last_analyzed :fingerprint_version))))
 
 (defmethod serdes/load-xform "Field"
   [field]
   (-> (serdes/load-xform-basics field)
       (update :table_id           serdes/*import-table-fk*)
-      (update :fk_target_field_id serdes/*import-field-fk*)))
+      (update :fk_target_field_id serdes/*import-field-fk*)
+      (update :parent_id          serdes/*import-field-fk*)))
 
 (defmethod serdes/load-find-local "Field"
   [path]
-- 
GitLab