From 98bbb00151ce0c2d232e220f6ae57e0082b28f8d Mon Sep 17 00:00:00 2001
From: Case Nelson <case@metabase.com>
Date: Wed, 10 Aug 2022 13:24:28 -0600
Subject: [PATCH] [Actions] Simplify emitter schema model (#24570)

* Move writeback migrations to 45

* Empty commit to trigger GitHub Actions

* [Actions] Simplify emitter schema model

emitter_action was dropped since emitters just have a singular action
and the join table was unecessary.

emitter_action.action_id columns moved onto emitter table.

Dropped CardEmitter and DashboardEmitter pre-insert, pre-update,
pre-delete since they were used in tests only and normal operation
would see the emitter inserted first.

Since previous code may have 'orphaned' emitters without an action, we
delete emitters without action to be safe.

* Handle flakiness with geojson java.net.UnknownHostException errors (#24523)

* Handle flakiness with geojson java.net.UnknownHostException errors

In CI seems like we are getting errant errors:

```clojure
geojson.clj:62
It validates URLs and files appropriately
http://0xc0000200
expected: (valid? geojson)
  actual: #error {
 :cause "Invalid IP address literal: 0xc0000200"
 :via
 [{:type clojure.lang.ExceptionInfo
   :message "Invalid GeoJSON file location: must either start with http:// or https:// or be a relative path to a file on the classpath. URLs referring to hosts that supply internal hosting metadata are prohibited."
   :data {:status-code 400, :url "http://0xc0000200"}
   :at [metabase.api.geojson$valid_url_QMARK_ invokeStatic "geojson.clj" 62]}
  {:type java.net.UnknownHostException
   :message "0xc0000200"
   :at [java.net.InetAddress getAllByName "InetAddress.java" 1340]}
  {:type java.lang.IllegalArgumentException
   :message "Invalid IP address literal: 0xc0000200"
   :at [sun.net.util.IPAddressUtil validateNumericFormatV4 "IPAddressUtil.java" 150]}]
```

Not clear if this change has a hope of fixing it: if it doesn't resolve
once its possible it is cached somewhere in the network stack, or it
won't resolve if you ask again.

But gonna give it a shot.

Set the property `"networkaddress.cache.negative.ttl"` to `"0"`

> networkaddress.cache.negative.ttl (default: 10)
>    Indicates the caching policy for un-successful name lookups from the name service. The value is specified as an integer to indicate the number of seconds to cache the failure for un-successful lookups.

>    A value of 0 indicates "never cache". A value of -1 indicates "cache forever".

From
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/InetAddress.html
in the hopes that we can try multiple times. Restores the original value
after the test completes so we don't inadvertently change behavior
elsewhere.

If we get an error of java.net.UnknownHostException we try again if we
have attempts remaining. If we get a boolean it means the ip resolution
worked so we can rely on the response (checking if it resolves locally
or not)

* add a delay

* comment out test

Co-authored-by: Cam Saul <github@camsaul.com>
Co-authored-by: dpsutton <dan@dpsutton.com>
---
 .../models/entity_id_test.clj                 |  1 -
 resources/migrations/000_migrations.yaml      | 63 +++++++++++++++++++
 src/metabase/api/emitter.clj                  | 12 ++--
 src/metabase/models.clj                       |  1 -
 src/metabase/models/action.clj                |  8 +--
 src/metabase/models/emitter.clj               | 53 +---------------
 test/metabase/actions/test_util.clj           |  7 +--
 test/metabase/api/card_test.clj               |  6 +-
 test/metabase/api/dashboard_test.clj          |  6 +-
 test/metabase/cmd/copy_test.clj               |  2 +-
 10 files changed, 86 insertions(+), 73 deletions(-)

diff --git a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj
index 03c5d50f27e..f8f5aaaee50 100644
--- a/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj
+++ b/enterprise/backend/test/metabase_enterprise/models/entity_id_test.clj
@@ -45,7 +45,6 @@
     metabase.models.emitter.CardEmitterInstance
     metabase.models.emitter.DashboardEmitterInstance
     metabase.models.emitter.EmitterInstance
-    metabase.models.emitter.EmitterActionInstance
     metabase.models.field_values.FieldValuesInstance
     metabase.models.login_history.LoginHistoryInstance
     metabase.models.metric_important_field.MetricImportantFieldInstance
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index 36999093f56..06d9a06d371 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -12542,6 +12542,69 @@ databaseChangeLog:
             columnNames: action_id
             constraintName: pk_http_action
 
+  - changeSet:
+      id: v45.00-014
+      author: snoe
+      comment: Added 0.45.0 - writeback add action_id to emitter
+      changes:
+        - addColumn:
+            columns:
+              - column:
+                  name: action_id
+                  type: int
+                  remarks: The related action
+            tableName: emitter
+
+  # FK constraint is added separately because deleteCascade doesn't work in addColumn -- see #14321
+  - changeSet:
+      id: v45.00-015
+      author: snoe
+      comment: Added 0.45.0 - writeback add fk for action_id to emitter
+      changes:
+        - addForeignKeyConstraint:
+            baseTableName: emitter
+            baseColumnNames: action_id
+            referencedTableName: action
+            referencedColumnNames: id
+            constraintName: fk_emitter_ref_action_id
+            onDelete: CASCADE
+
+  - changeSet:
+      id: v45.00-016
+      author: snoe
+      comment: Added 0.45.0 - writeback Move foreign keys from emitter_action and dashboard_emitter
+      changes:
+        - sql:
+              sql: >-
+                update emitter set
+                action_id = (select action_id from emitter_action where emitter_id = id);
+
+  - changeSet:
+      id: v45.00-017
+      author: snoe
+      comment: Added 0.45.0 - writeback remove any emitters that were orphaned to be safe.
+      changes:
+        - sql:
+              sql: delete from emitter where action_id is null;
+
+  - changeSet:
+      id: v45.00-018
+      author: snoe
+      comment: Added 0.45.0 - writeback Make emitter action_id not nullable
+      changes:
+        - addNotNullConstraint:
+            columnDataType: int
+            tableName: emitter
+            columnName: action_id
+
+  - changeSet:
+      id: v45.00-019
+      author: snoe
+      comment: Added 0.45.0 - writeback drop emitter_action
+      changes:
+        - dropTable:
+            tableName: emitter_action
+
 # >>>>>>>>>> DO NOT ADD NEW MIGRATIONS BELOW THIS LINE! ADD THEM ABOVE <<<<<<<<<<
 
 ########################################################################################################################
diff --git a/src/metabase/api/emitter.clj b/src/metabase/api/emitter.clj
index 1bbc5e752ed..abdc5f670a5 100644
--- a/src/metabase/api/emitter.clj
+++ b/src/metabase/api/emitter.clj
@@ -4,8 +4,7 @@
             [metabase.actions.http-action :as http-action]
             [metabase.api.common :as api]
             [metabase.mbql.schema :as mbql.s]
-            [metabase.models
-             :refer [CardEmitter DashboardEmitter Emitter EmitterAction]]
+            [metabase.models :refer [CardEmitter DashboardEmitter Emitter]]
             [metabase.query-processor.writeback :as qp.writeback]
             [metabase.util :as u]
             [metabase.util.i18n :refer [tru]]
@@ -19,9 +18,7 @@
   [emitter-id]
   (when-let [emitter (->> (db/query {:select    [:*]
                                      :from      [[Emitter :emitter]]
-                                     :left-join [[EmitterAction :emitter_action]
-                                                 [:= :emitter_action.emitter_id :emitter.id]
-                                                 ;; TODO -- not 100% sure we need CardEmitter and DashboardEmitter, we'd
+                                     :left-join [;; TODO -- not 100% sure we need CardEmitter and DashboardEmitter, we'd
                                                  ;; only need that if we wanted to hydrate the Card or Dashboard they
                                                  ;; came from.
                                                  [CardEmitter :card_emitter]
@@ -44,8 +41,9 @@
   ;; Create stuff the hard way by hand because H2 doesn't return the Emitter ID if you have Toucan `pre-insert` create
   ;; them for you because of some issue with INSERT RETURNING ROWS or something like that. See
   ;; [[metabase.models.emitter/pre-insert]] for more discussion.
-  (let [emitter-id (u/the-id (db/insert! Emitter {:options options, :parameter_mappings parameter_mappings}))]
-    (db/insert! EmitterAction {:emitter_id emitter-id, :action_id action_id})
+  (let [emitter-id (u/the-id (db/insert! Emitter {:options options,
+                                                  :parameter_mappings parameter_mappings
+                                                  :action_id action_id}))]
     (cond
       dashboard_id
       (db/insert! DashboardEmitter {:emitter_id emitter-id, :dashboard_id dashboard_id})
diff --git a/src/metabase/models.clj b/src/metabase/models.clj
index 8497ebb1eb9..28b184c6999 100644
--- a/src/metabase/models.clj
+++ b/src/metabase/models.clj
@@ -105,7 +105,6 @@
  [emitter CardEmitter]
  [emitter DashboardEmitter]
  [emitter Emitter]
- [emitter EmitterAction]
  [field Field]
  [field-values FieldValues]
  [login-history LoginHistory]
diff --git a/src/metabase/models/action.clj b/src/metabase/models/action.clj
index 5546f0af655..6db464fb695 100644
--- a/src/metabase/models/action.clj
+++ b/src/metabase/models/action.clj
@@ -132,10 +132,8 @@
   [emitters]
   ;; emitters apparently might actually be `[nil]` (not 100% sure why) so just make sure we're not doing anything dumb
   ;; if this is the case.
-  (if-let [emitter-ids (seq (filter some? (map :id emitters)))]
-    (let [emitter-actions (db/select 'EmitterAction :emitter_id [:in emitter-ids])
-          action-id-by-emitter-id (into {} (map (juxt :emitter_id :action_id) emitter-actions))
-          actions (m/index-by :id (select-actions :id [:in (map :action_id emitter-actions)]))]
+  (if-let [action-id-by-emitter-id (not-empty (into {} (map (juxt :id :action_id) (filter :id emitters))))]
+    (let [actions-by-id (m/index-by :id (select-actions :id [:in (map val action-id-by-emitter-id)]))]
       (for [{emitter-id :id, :as emitter} emitters]
-        (some-> emitter (assoc :action (get actions (get action-id-by-emitter-id emitter-id))))))
+        (some-> emitter (assoc :action (get actions-by-id (get action-id-by-emitter-id emitter-id))))))
     emitters))
diff --git a/src/metabase/models/emitter.clj b/src/metabase/models/emitter.clj
index 867f56be756..3cc604b4fc0 100644
--- a/src/metabase/models/emitter.clj
+++ b/src/metabase/models/emitter.clj
@@ -12,7 +12,6 @@
 (models/defmodel CardEmitter :card_emitter)
 (models/defmodel DashboardEmitter :dashboard_emitter)
 (models/defmodel Emitter :emitter)
-(models/defmodel EmitterAction :emitter_action)
 
 (defn- normalize-parameter-mappings
   [parameter-mappings]
@@ -33,58 +32,11 @@
                                          :options            :json})
             :properties     (constantly {:timestamped? true})}))
 
-(u/strict-extend
-  (class EmitterAction)
-  models/IModel
-  (merge models/IModelDefaults
-         ;; This is ok as long as we're 1:1
-         {:primary-key (constantly :emitter_id)}))
-
-(defn- pre-insert
-  "We currently support two ways of creating a CardEmitter or DashboardEmitter:
-
-   1. Automatically create the base `Emitter` and `EmitterAction` at the same time. Pass in `:action_id` but not
-      `:emitter_id`. This was the original way we coded this but it actually causes a few issues since H2 doesn't seem
-      to return the newly created object when you do things this way
-
-   2. Create the base `Emitter` and `EmitterAction` by hand. Pass in `:emitter_id` but not `:action_id`. This is
-      probably preferable going forward because H2 does the right thing when you do it this way."
-  [{action-id :action_id, emitter-id :emitter_id, :as emitter}]
-  ;; if `emitter_id` was passed then assume the Base `Emitter` was created manually. Otherwise, create one
-  ;; automatically.
-  (let [base-emitter (if emitter-id
-                       (db/select-one Emitter :id emitter-id)
-                       (db/insert! Emitter (select-keys emitter [:parameter_mappings :options])))]
-    ;; if `action_id` was passed then automatically create an `EmitterAction`. Otherwise assume it was created manually.
-    (when action-id
-      (db/insert! EmitterAction {:action_id action-id, :emitter_id (u/the-id base-emitter)}))
-    ;; ok now Toucan should be able to create the `CardEmitter`/`DashboardEmitter` for us.
-    (-> emitter
-        (select-keys [:dashboard_id :card_id])
-        (assoc :emitter_id (u/the-id base-emitter)))))
-
-(defn- pre-update
-  [emitter]
-  (when-some [base-emitter (not-empty (select-keys emitter [:parameter_mappings :options]))]
-    (db/update! Emitter (:emitter_id emitter) base-emitter))
-  (when-some [emitter-action (-> emitter
-                                 (select-keys [:action_id])
-                                 not-empty)]
-    (db/update! EmitterAction (:emitter_id emitter) emitter-action))
-  (not-empty (select-keys emitter [:emitter_id :dashboard_id :card_id])))
-
-(defn- pre-delete
-  [emitter]
-  (db/delete! Emitter :id (:emitter_id emitter))
-  emitter)
-
 (def ^:private Emitter-subtype-IModel-impl
   "[[models/IModel]] impl for `DashboardEmitter` and `CardEmitter`"
   (merge models/IModelDefaults
-         {:primary-key (constantly :emitter_id) ; This is ok as long as we're 1:1
-          :pre-delete  pre-delete
-          :pre-update  pre-update
-          :pre-insert  pre-insert}))
+         ; This is ok as long as we're 1:1
+         {:primary-key (constantly :emitter_id)}))
 
 (u/strict-extend (class DashboardEmitter)
   models/IModel
@@ -107,6 +59,7 @@
     (let [qualified-join-column (keyword (str "typed_emitter." (name join-column)))
           emitters (->> {:select [qualified-join-column
                                   :emitter.id
+                                  :emitter.action_id
                                   :emitter.options
                                   :emitter.parameter_mappings
                                   :emitter.created_at
diff --git a/test/metabase/actions/test_util.clj b/test/metabase/actions/test_util.clj
index e2058cf17b9..f8860662d66 100644
--- a/test/metabase/actions/test_util.clj
+++ b/test/metabase/actions/test_util.clj
@@ -5,7 +5,7 @@
    [metabase.driver.sql-jdbc.connection :as sql-jdbc.conn]
    [metabase.http-client :as client]
    [metabase.models :refer [Action Card CardEmitter Dashboard DashboardEmitter
-                            Database Emitter EmitterAction QueryAction]]
+                            Database Emitter QueryAction]]
    [metabase.models.action :as action]
    [metabase.test :as mt]
    [metabase.test.data.dataset-definitions :as defs]
@@ -168,7 +168,8 @@
   (let [parent-model (db/resolve-model card-or-dashboard-model)]
     (mt/with-temp* [parent-model [{emitter-parent-id :id}]
                     Emitter [{emitter-id :id} {:parameter_mappings {"my_id" [:variable [:template-tag "id"]]
-                                                                    "my_fail" [:variable [:template-tag "fail"]]}}]]
+                                                                    "my_fail" [:variable [:template-tag "fail"]]}
+                                               :action_id action-id}]]
       (testing "Sanity check: emitter-id should be non-nil"
         (is (integer? emitter-id)))
       (testing "Sanity check: make sure parameter mappings were defined the way we'd expect"
@@ -177,8 +178,6 @@
                (db/select-one-field :parameter_mappings Emitter :id emitter-id))))
       ;; these are tied to the Card or Dashboad and Emitter above and will get cascade deleted. We can't use `with-temp*` for them
       ;; because it doesn't seem to work with tables with compound PKs
-      (db/insert! EmitterAction {:emitter_id emitter-id
-                                 :action_id action-id})
       (condp = (type parent-model)
         (type Card)
         (db/insert! CardEmitter {:card_id    emitter-parent-id
diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj
index 59259b34900..ff3880933dc 100644
--- a/test/metabase/api/card_test.clj
+++ b/test/metabase/api/card_test.clj
@@ -19,6 +19,7 @@
                                      Collection
                                      Dashboard
                                      Database
+                                     Emitter
                                      ModerationReview
                                      PersistedInfo
                                      Pulse
@@ -754,8 +755,9 @@
   (testing "GET /api/card/:id"
     (testing "Fetch card with an emitter"
       (mt/with-temp* [Card [read-card {:name "Test Read Card"}]
-                      Card [write-card {:is_write true :name "Test Write Card"}]]
-        (db/insert! CardEmitter {:action_id (u/the-id (db/select-one-field :action_id QueryAction :card_id (u/the-id write-card)))
+                      Card [write-card {:is_write true :name "Test Write Card"}]
+                      Emitter [{emitter-id :id} {:action_id (u/the-id (db/select-one-field :action_id QueryAction :card_id (u/the-id write-card)))}]]
+        (db/insert! CardEmitter {:emitter_id emitter-id
                                  :card_id (u/the-id read-card)})
         (testing "admin sees emitters"
           (is (partial=
diff --git a/test/metabase/api/dashboard_test.clj b/test/metabase/api/dashboard_test.clj
index b7008af020c..2159615ba90 100644
--- a/test/metabase/api/dashboard_test.clj
+++ b/test/metabase/api/dashboard_test.clj
@@ -15,6 +15,7 @@
                                      DashboardCard
                                      DashboardCardSeries
                                      DashboardEmitter
+                                     Emitter
                                      Field
                                      FieldValues
                                      Pulse
@@ -345,8 +346,9 @@
   (testing "GET /api/dashboard/:id"
     (testing "Fetch dashboard with an emitter"
       (mt/with-temp* [Dashboard [dashboard {:name "Test Dashboard"}]
-                      Card [write-card {:is_write true :name "Test Write Card"}]]
-        (db/insert! DashboardEmitter {:action_id    (u/the-id (db/select-one-field :action_id QueryAction :card_id (u/the-id write-card)))
+                      Card [write-card {:is_write true :name "Test Write Card"}]
+                      Emitter [{emitter-id :id} {:action_id (u/the-id (db/select-one-field :action_id QueryAction :card_id (u/the-id write-card)))}]]
+        (db/insert! DashboardEmitter {:emitter_id emitter-id
                                       :dashboard_id (u/the-id dashboard)})
         (testing "admin sees emitters"
           (is (partial=
diff --git a/test/metabase/cmd/copy_test.clj b/test/metabase/cmd/copy_test.clj
index 2173f8a3c94..f420566e167 100644
--- a/test/metabase/cmd/copy_test.clj
+++ b/test/metabase/cmd/copy_test.clj
@@ -26,7 +26,7 @@
   (let [migrated-model-names (set (map :name copy/entities))
         ;; Models that should *not* be migrated in `load-from-h2`.
         models-to-exclude    #{"TaskHistory" "Query" "QueryCache" "QueryExecution" "CardFavorite" "DashboardFavorite"
-                               "Action" "HTTPAction" "QueryAction" "CardEmitter" "DashboardEmitter" "EmitterAction"}
+                               "Action" "HTTPAction" "QueryAction" "CardEmitter" "DashboardEmitter"}
         all-model-names      (set (for [ns       u/metabase-namespace-symbols
                                         :when    (or (re-find #"^metabase\.models\." (name ns))
                                                      (= (name ns) "metabase.db.data-migrations"))
-- 
GitLab