This is an automated email from the ASF dual-hosted git repository.

bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new d0b3d5d42f Issue #9136 : Keep a failed SQL statement from crashing Hop 
Web (#9141)
d0b3d5d42f is described below

commit d0b3d5d42ffa0da3cbacd3642532cf52592a59b6
Author: Matt Casters <[email protected]>
AuthorDate: Fri Oct 30 10:35:29 2026 +0100

    Issue #9136 : Keep a failed SQL statement from crashing Hop Web (#9141)
---
 .../org/apache/hop/core/SwtUniversalImage.java     |  7 +-
 .../org/apache/hop/ui/core/gui/GuiResource.java    | 23 ++++++
 .../perspective/database/DatabaseOperation.java    |  6 ++
 .../perspective/database/DatabasePerspective.java  |  4 +-
 .../perspective/database/DatabaseSqlEditorTab.java | 89 ++++++++++++++++++++--
 .../perspective/database/DatabaseSqlTabMemory.java | 21 ++++-
 .../perspective/database/DatabaseTableInfoTab.java | 10 ++-
 .../perspective/database/DatabaseWorkbench.java    | 45 +++++++++--
 .../database/HopGuiDatabaseWorkbenchHost.java      |  4 +-
 .../org/apache/hop/core/SwtUniversalImageTest.java | 34 +++++++++
 .../database/DatabaseOperationsPanelTest.java      | 18 +++++
 .../database/DatabaseSqlEditorTabTest.java         | 33 ++++++++
 12 files changed, 273 insertions(+), 21 deletions(-)

diff --git a/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java 
b/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
index d1b679bba5..1b07815d31 100644
--- a/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
+++ b/ui/src/main/java/org/apache/hop/core/SwtUniversalImage.java
@@ -63,8 +63,13 @@ public abstract class SwtUniversalImage {
     cache = null;
   }
 
+  /** True after {@link #dispose()}, including when the display has already 
released this image. */
+  public synchronized boolean isDisposed() {
+    return cache == null;
+  }
+
   private void checkDisposed() {
-    if (cache == null) {
+    if (isDisposed()) {
       throw new HopRuntimeException("Already disposed");
     }
   }
diff --git a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java 
b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
index 1c4edb5bb0..8ca82a44bf 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/gui/GuiResource.java
@@ -76,6 +76,13 @@ public class GuiResource {
 
   private final Display display;
 
+  /**
+   * Set at the start of {@link #dispose()}. Display shutdown fires {@code 
SWT.Dispose} and releases
+   * these images before it disposes shells, so callers can still see a live 
widget whose icon is
+   * already gone.
+   */
+  private boolean disposed;
+
   private double zoomFactor;
 
   // Colors
@@ -442,7 +449,16 @@ public class GuiResource {
     // Transform, action, database and value type icons are loaded when they 
are first asked for
   }
 
+  /** True once display shutdown has released the fonts and images held here. 
*/
+  public boolean isDisposed() {
+    return disposed;
+  }
+
   private void dispose() {
+    if (disposed) {
+      return;
+    }
+    disposed = true;
     // display shutdown, clean up the fonts, images, colors, and so on.
     //
     // Fonts
@@ -1259,6 +1275,13 @@ public class GuiResource {
 
   private Image getZoomedImaged(
       SwtUniversalImage universalImage, Device device, int width, int height) {
+    if (disposed
+        || universalImage == null
+        || universalImage.isDisposed()
+        || device == null
+        || device.isDisposed()) {
+      return null;
+    }
     return universalImage.getAsBitmapForSize(
         device,
         ConstUi.zoomedIconSize(width, zoomFactor),
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
index 9ce401ba12..bfbc4b7639 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
@@ -86,6 +86,12 @@ public class DatabaseOperation {
   }
 
   public void complete() {
+    // A statement failure marks the operation failed and returns. Completing 
afterwards must not
+    // turn that into success.
+    if (status != Status.RUNNING) {
+      detachDatabase();
+      return;
+    }
     status = cancelled ? Status.CANCELLED : Status.DONE;
     endTime = System.currentTimeMillis();
     detachDatabase();
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabasePerspective.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabasePerspective.java
index b3001c1ca8..ec0e7a92c8 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabasePerspective.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabasePerspective.java
@@ -293,10 +293,10 @@ public class DatabasePerspective implements 
IHopPerspective, TabClosable, IDatab
     }
     display.asyncExec(
         () -> {
-          if (workbench == null || workbench.isDisposed()) {
+          if (display.isDisposed() || workbench == null || 
workbench.isDisposed()) {
             return;
           }
-          runnable.run();
+          DatabaseWorkbench.runSafely(runnable);
         });
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
index cdf523cf2d..7aa1efc809 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
@@ -63,6 +63,8 @@ import org.apache.hop.ui.hopgui.shared.SashFormMemory;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
@@ -467,6 +469,9 @@ public class DatabaseSqlEditorTab implements 
IHopFileTypeHandler {
               nr++;
               if (sql.isQuery()) {
                 List<Object[]> rows = db.getRows(sql.getStatement(), rowLimit);
+                if (rows == null) {
+                  rows = List.of();
+                }
                 IRowMeta rowMeta = db.getReturnRowMeta();
                 queryResults.add(new DatabaseResultsPanel.QueryResult(nr, 
rowMeta, rows));
                 messages.append(queryResultMessage(nr, rows.size(), 
rowLimit)).append(Const.CR);
@@ -480,19 +485,23 @@ public class DatabaseSqlEditorTab implements 
IHopFileTypeHandler {
               }
             }
           } catch (Exception e) {
+            // A bad statement is a result, not a GUI failure. Rethrowing it 
makes runOperation log
+            // the stack, and on Hop Web that takes the session down (issue 
#9136).
             if (!messages.isEmpty() && messages.charAt(messages.length() - 1) 
!= '\n') {
               messages.append(Const.CR);
             }
+            String failure = sqlFailureText(e);
             messages
                 .append(BaseMessages.getString(PKG, 
"DatabasePerspective.SqlTab.Failed"))
                 .append(": ")
-                .append(Const.NVL(e.getMessage(), e.toString()));
-            throw e;
+                .append(Const.NVL(e.getMessage(), failure));
+            operation.fail(failure);
+            LogChannel.UI.logError(description + ": " + failure);
           } finally {
             String messageText = messages.toString();
             host.asyncExec(
                 () -> {
-                  if (control.isDisposed() || resultsPanel.isDisposed()) {
+                  if (control.isDisposed() || resultsPanel.isDisposed() || 
sash.isDisposed()) {
                     return;
                   }
                   resultsPanel.show(queryResults, messageText);
@@ -518,6 +527,59 @@ public class DatabaseSqlEditorTab implements 
IHopFileTypeHandler {
     return 
DatabasePerspectiveConfigSingleton.getConfig().resolvedQueryRowLimit();
   }
 
+  /**
+   * One line for the operations status and the log. The results panel keeps 
{@code
+   * Throwable#getMessage()}, which includes the SQL. The root cause is the 
database text ({@code
+   * ERROR: relation "..." does not exist}); the first line of that is enough 
outside the panel.
+   */
+  static String sqlFailureText(Throwable error) {
+    if (error == null) {
+      return "";
+    }
+    Throwable root = error;
+    while (root.getCause() != null && root.getCause() != root) {
+      root = root.getCause();
+    }
+    String message = root.getMessage();
+    if (Utils.isEmpty(message)) {
+      message = error.getMessage();
+    }
+    if (Utils.isEmpty(message)) {
+      return root.toString();
+    }
+    int newline = message.indexOf('\n');
+    if (newline >= 0) {
+      message = message.substring(0, newline);
+    }
+    int cr = message.indexOf('\r');
+    if (cr >= 0) {
+      message = message.substring(0, cr);
+    }
+    return message.trim();
+  }
+
+  /**
+   * Display shutdown and a closed editor both surface as one of these. They 
are not defects to log
+   * again while the session is already going away.
+   */
+  static boolean isShutdownRace(Throwable error) {
+    Throwable current = error;
+    while (current != null) {
+      String message = current.getMessage();
+      if (message != null
+          && (message.contains("Widget is disposed")
+              || message.contains("Already disposed")
+              || message.contains("Device is disposed"))) {
+        return true;
+      }
+      if (current.getCause() == current) {
+        break;
+      }
+      current = current.getCause();
+    }
+    return false;
+  }
+
   static String queryResultMessage(int statementNr, int rowCount, int 
rowLimit) {
     if (rowLimit > 0 && rowCount >= rowLimit) {
       return BaseMessages.getString(
@@ -556,10 +618,25 @@ public class DatabaseSqlEditorTab implements 
IHopFileTypeHandler {
     if (tabItem == null || tabItem.isDisposed()) {
       return;
     }
+    GuiResource resources = GuiResource.getInstance();
+    if (resources.isDisposed()) {
+      return;
+    }
     tabItem.setText(Const.NVL(getName(), ""));
-    tabItem.setImage(GuiResource.getInstance().getImageFile());
-    tabItem.setFont(
-        changed ? GuiResource.getInstance().getFontBold() : 
tabItem.getParent().getFont());
+    Image image = resources.getImageFile();
+    if (image != null && !image.isDisposed()) {
+      tabItem.setImage(image);
+    }
+    Font font = tabItem.getParent() == null ? null : 
tabItem.getParent().getFont();
+    if (changed) {
+      Font bold = resources.getFontBold();
+      if (bold != null && !bold.isDisposed()) {
+        font = bold;
+      }
+    }
+    if (font != null && !font.isDisposed()) {
+      tabItem.setFont(font);
+    }
   }
 
   public void setChanged() {
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
index ca1224da80..57e4d3543f 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
@@ -28,6 +28,7 @@ import org.apache.hop.history.AuditList;
 import org.apache.hop.history.AuditManager;
 import org.apache.hop.history.AuditState;
 import org.apache.hop.history.AuditStateMap;
+import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.HopNamespace;
 import org.apache.hop.ui.hopgui.HopGui;
 import org.eclipse.swt.widgets.Display;
@@ -59,8 +60,21 @@ final class DatabaseSqlTabMemory {
 
   private DatabaseSqlTabMemory() {}
 
+  /**
+   * False while the display is shutting down. {@code Display.isDisposed()} is 
still false then: the
+   * disposed flag is set only after shells have been released.
+   */
+  private static boolean guiResourcesUsable() {
+    try {
+      GuiResource resources = GuiResource.getInstance();
+      return resources != null && !resources.isDisposed();
+    } catch (RuntimeException e) {
+      return false;
+    }
+  }
+
   static void restore(DatabaseWorkbench workbench) {
-    if (workbench == null || workbench.isDisposed()) {
+    if (workbench == null || workbench.isDisposed() || !guiResourcesUsable()) {
       return;
     }
     register(workbench);
@@ -256,6 +270,11 @@ final class DatabaseSqlTabMemory {
   }
 
   static void restoreIntoRemaining(DatabaseWorkbench leaving) {
+    // Display.release() disposes GuiResource (SWT.Dispose) before it disposes 
shells. Opening a
+    // tab from this dispose listener then asks for an icon that is already 
gone (issue #9136).
+    if (!guiResourcesUsable()) {
+      return;
+    }
     Session session = session(leaving);
     if (session == null) {
       return;
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
index 2397c0217e..cb7cd8168e 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
@@ -164,9 +164,13 @@ public class DatabaseTableInfoTab implements 
IHopFileTypeHandler {
 
   public void setTabItem(CTabItem tabItem) {
     this.tabItem = tabItem;
-    if (tabItem != null && !tabItem.isDisposed()) {
-      tabItem.setText(getName());
-      tabItem.setImage(tabImage());
+    if (tabItem == null || tabItem.isDisposed() || 
GuiResource.getInstance().isDisposed()) {
+      return;
+    }
+    tabItem.setText(getName());
+    Image image = tabImage();
+    if (image != null && !image.isDisposed()) {
+      tabItem.setImage(image);
     }
   }
 
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
index c6b7a8cf76..09695ab4a4 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
@@ -1622,17 +1622,50 @@ public class DatabaseWorkbench extends Composite 
implements TabClosable {
     BackgroundThreadFacade.start(
         () -> {
           try {
-            work.run(operation);
-            operation.complete();
-          } catch (Exception e) {
-            LogChannel.UI.logError(description, e);
-            operation.fail(Const.NVL(e.getMessage(), e.toString()));
+            try {
+              work.run(operation);
+              operation.complete();
+            } catch (Exception e) {
+              // The SQL editor records a statement failure itself and does 
not throw. Anything
+              // that still escapes is unexpected.
+              if (operation.getStatus() == DatabaseOperation.Status.RUNNING) {
+                LogChannel.UI.logError(description, e);
+                operation.fail(Const.NVL(e.getMessage(), e.toString()));
+              }
+            }
+            host.asyncExec(this::refreshOperationsQuietly);
+          } catch (RuntimeException e) {
+            if (operation.getStatus() == DatabaseOperation.Status.RUNNING) {
+              operation.fail(Const.NVL(e.getMessage(), e.toString()));
+            }
+            if (!DatabaseSqlEditorTab.isShutdownRace(e)) {
+              LogChannel.UI.logError(description, e);
+            }
           }
-          host.asyncExec(operationsPanel::refresh);
         },
         "hop-database-op");
   }
 
+  private void refreshOperationsQuietly() {
+    if (operationsPanel.isDisposed()) {
+      return;
+    }
+    operationsPanel.refresh();
+  }
+
+  /** Runs a UI callback without letting a disposed widget or image kill the 
Hop Web session. */
+  static void runSafely(Runnable runnable) {
+    try {
+      if (runnable != null) {
+        runnable.run();
+      }
+    } catch (RuntimeException e) {
+      if (!DatabaseSqlEditorTab.isShutdownRace(e)) {
+        LogChannel.UI.logError("Database workbench UI update failed", e);
+      }
+    }
+  }
+
   public void refreshTab(DatabaseSqlEditorTab tab) {
     tab.setTabItem(tab.getTabItem());
     host.updateGui(tab);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
index a56492d9b1..ae015dc545 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
@@ -94,10 +94,10 @@ public class HopGuiDatabaseWorkbenchHost implements 
IDatabaseWorkbenchHost {
     }
     display.asyncExec(
         () -> {
-          if (alive != null && !alive.getAsBoolean()) {
+          if (display.isDisposed() || (alive != null && 
!alive.getAsBoolean())) {
             return;
           }
-          runnable.run();
+          DatabaseWorkbench.runSafely(runnable);
         });
   }
 
diff --git a/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java 
b/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java
index da2a413384..9e7ddd7f12 100644
--- a/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java
+++ b/ui/src/test/java/org/apache/hop/core/SwtUniversalImageTest.java
@@ -19,10 +19,14 @@ package org.apache.hop.core;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.awt.image.BufferedImage;
 import java.lang.reflect.Method;
+import org.apache.hop.core.exception.HopRuntimeException;
 import org.eclipse.swt.graphics.Device;
+import org.eclipse.swt.graphics.Image;
 import org.eclipse.swt.graphics.ImageData;
 import org.junit.jupiter.api.Test;
 
@@ -37,6 +41,18 @@ class SwtUniversalImageTest {
     assertEquals(1, SwtUniversalImage.pixelSize(0, 200));
   }
 
+  @Test
+  void disposedImageReportsDisposedAndRefusesAnotherBitmap() {
+    SwtUniversalImage image = new UnusedImage();
+    assertFalse(image.isDisposed());
+    image.dispose();
+    assertTrue(image.isDisposed());
+    image.dispose();
+    HopRuntimeException thrown =
+        assertThrows(HopRuntimeException.class, () -> 
image.getAsBitmapForSize(null, 16, 16));
+    assertEquals("Already disposed", thrown.getMessage());
+  }
+
   @Test
   void toImageDataKeepsSizeAndAlpha() {
     BufferedImage src = new BufferedImage(16, 8, BufferedImage.TYPE_INT_ARGB);
@@ -59,4 +75,22 @@ class SwtUniversalImageTest {
           "RAP class loading resolves method signatures; keep 
ImageDataProvider off this API");
     }
   }
+
+  /** Never rendered. Dispose only clears the cache. */
+  private static final class UnusedImage extends SwtUniversalImage {
+    @Override
+    protected Image renderSimple(Device device) {
+      return null;
+    }
+
+    @Override
+    protected Image renderSimple(Device device, int width, int height) {
+      return null;
+    }
+
+    @Override
+    protected Image renderRotated(Device device, int width, int height, double 
angleRadians) {
+      return null;
+    }
+  }
 }
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
index 87d8df2e86..f10f4e0bd8 100644
--- 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
@@ -69,6 +69,24 @@ class DatabaseOperationsPanelTest {
     assertEquals("", DatabaseOperationsPanel.formatStatusLine(null));
   }
 
+  @Test
+  void completeLeavesAFailureInPlace() {
+    DatabaseOperation operation = new DatabaseOperation("Execute SQL on CRM", 
"CRM");
+    operation.fail("ERROR: relation \"order_shipment_tracking\" does not 
exist");
+    operation.complete();
+    assertEquals(DatabaseOperation.Status.FAILED, operation.getStatus());
+    assertEquals(
+        "ERROR: relation \"order_shipment_tracking\" does not exist", 
operation.getErrorMessage());
+  }
+
+  @Test
+  void completeOfACancelledRunIsCancelled() {
+    DatabaseOperation operation = new DatabaseOperation("Execute SQL on CRM", 
"CRM");
+    operation.cancel();
+    operation.complete();
+    assertEquals(DatabaseOperation.Status.CANCELLED, operation.getStatus());
+  }
+
   @Test
   void failDetachesDatabase() {
     DatabaseOperation operation = new DatabaseOperation("Connect", "shop");
diff --git 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTabTest.java
 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTabTest.java
index f82fc05f49..584f38ba55 100644
--- 
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTabTest.java
+++ 
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTabTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import org.apache.hop.core.exception.HopDatabaseException;
 import org.junit.jupiter.api.Test;
 
 class DatabaseSqlEditorTabTest {
@@ -59,6 +60,38 @@ class DatabaseSqlEditorTabTest {
     assertTrue(DatabaseSqlEditorTab.isModifiedFromClean("SELECT 1", ""));
   }
 
+  @Test
+  void sqlFailureTextUsesTheDatabaseMessage() {
+    Exception cause =
+        new Exception("ERROR: relation \"order_shipment_tracking\" does not 
exist\n  Position: 64");
+    String text =
+        DatabaseSqlEditorTab.sqlFailureText(
+            new HopDatabaseException("An error occurred executing SQL: 
\nSELECT 1", cause));
+    assertEquals("ERROR: relation \"order_shipment_tracking\" does not exist", 
text);
+  }
+
+  @Test
+  void sqlFailureTextKeepsASingleLineAndSurvivesANull() {
+    assertEquals(
+        "relation missing",
+        DatabaseSqlEditorTab.sqlFailureText(new Exception("relation 
missing\r\nmore")));
+    assertEquals("", DatabaseSqlEditorTab.sqlFailureText(null));
+  }
+
+  @Test
+  void shutdownRaceIsNotAQueryFailure() {
+    assertTrue(DatabaseSqlEditorTab.isShutdownRace(new 
IllegalStateException("Already disposed")));
+    assertTrue(
+        DatabaseSqlEditorTab.isShutdownRace(new IllegalStateException("Widget 
is disposed")));
+    assertTrue(
+        DatabaseSqlEditorTab.isShutdownRace(
+            new RuntimeException("wrapper", new IllegalStateException("Device 
is disposed"))));
+    assertFalse(
+        DatabaseSqlEditorTab.isShutdownRace(
+            new HopDatabaseException("ERROR: relation \"t\" does not exist")));
+    assertFalse(DatabaseSqlEditorTab.isShutdownRace(null));
+  }
+
   @Test
   void generatedSqlCaretSitsOnTheLastStatement() {
     String statement = "CREATE TABLE t (id INT);";

Reply via email to