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

hansva 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 37844d7c15 improvements to database explorer and preview, fixes #6998 
(#7004)
37844d7c15 is described below

commit 37844d7c158006f61f34c039a7d4f95b83c5ba26
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Apr 15 12:53:33 2026 +0200

    improvements to database explorer and preview, fixes #6998 (#7004)
---
 core/src/main/java/org/apache/hop/core/Const.java  |  13 +
 .../org/apache/hop/core/database/Database.java     |  31 ++
 .../pages/pipeline/transforms/gettablenames.adoc   |   6 +
 .../ROOT/pages/pipeline/transforms/tableinput.adoc |   7 +
 .../modules/ROOT/pages/variables.adoc              |   1 +
 .../transforms/gettablenames/GetTableNames.java    |  11 +
 .../gettablenames/GetTableNamesDialog.java         |  67 +--
 .../pipeline/transforms/tableinput/TableInput.java |  13 +
 .../transforms/tableinput/TableInputDialog.java    |  98 ++--
 .../hop/ui/core/database/DatabaseMetaEditor.java   |   4 +-
 .../database/dialog/DatabaseExplorerDialog.java    | 553 ++++++++++++---------
 .../dialog/GetDatabaseInfoProgressDialog.java      |   8 +-
 .../dialog/GetPreviewTableProgressDialog.java      | 194 +++++---
 .../dialog/PreviewTableSettingsDialog.java         | 234 +++++++++
 .../hop/ui/core/database/dialog/SqlEditor.java     | 331 ++++++++----
 .../org/apache/hop/ui/core/dialog/BaseDialog.java  |  35 +-
 .../hop/ui/core/dialog/EnterNumberDialog.java      |  35 +-
 .../hop/ui/core/dialog/PreviewRowsDialog.java      |  38 +-
 .../hop/ui/core/dialog/ProgressMonitorDialog.java  | 208 +++++---
 .../hop/ui/core/metadata/MetadataEditorDialog.java |   1 +
 .../dialog/messages/messages_en_US.properties      |   8 +
 21 files changed, 1260 insertions(+), 636 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/Const.java 
b/core/src/main/java/org/apache/hop/core/Const.java
index af2bf805d4..32c2ba8e38 100644
--- a/core/src/main/java/org/apache/hop/core/Const.java
+++ b/core/src/main/java/org/apache/hop/core/Const.java
@@ -1005,6 +1005,19 @@ public class Const {
   public static final String HOP_TABLE_VIEW_EXTRA_COLUMN_MARGIN =
       "HOP_TABLE_VIEW_EXTRA_COLUMN_MARGIN";
 
+  /**
+   * Default JDBC {@link java.sql.Statement#setQueryTimeout(int)} in whole 
seconds for database
+   * <strong>query preview</strong> UIs (for example the initial value in the 
preview settings
+   * dialog). Normal pipeline execution does not use this variable for Table 
Input statement
+   * timeouts. Set in Hop configuration, project, environment, or pipeline 
variables.
+   */
+  @Variable(
+      scope = VariableScope.APPLICATION,
+      value = "20",
+      description =
+          "Default JDBC statement query timeout in seconds for database query 
preview (0 = unset).")
+  public static final String HOP_QUERY_PREVIEW_TIMEOUT = 
"HOP_QUERY_PREVIEW_TIMEOUT";
+
   /**
    * rounds double f to any number of places after decimal point Does 
arithmetic using BigDecimal
    * class to avoid integer overflow while rounding
diff --git a/core/src/main/java/org/apache/hop/core/database/Database.java 
b/core/src/main/java/org/apache/hop/core/database/Database.java
index 2332bd5415..4a190dcac3 100644
--- a/core/src/main/java/org/apache/hop/core/database/Database.java
+++ b/core/src/main/java/org/apache/hop/core/database/Database.java
@@ -121,6 +121,14 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
   private static final String CONST_ERROR_UPDATING_BATCH = "Error updating 
batch";
 
   private int rowlimit;
+
+  /**
+   * When positive, applied to statements created in {@link #openQuery(String, 
IRowMeta, Object[],
+   * int, boolean)} via {@link Statement#setQueryTimeout(int)} (whole 
seconds). Zero leaves the JDBC
+   * driver default (typically unlimited). Intended for short-lived GUI 
preview connections.
+   */
+  private int statementQueryTimeoutSeconds;
+
   private int commitsize;
 
   private Connection connection;
@@ -205,6 +213,7 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
     dbmd = null;
 
     rowlimit = 0;
+    statementQueryTimeoutSeconds = 0;
 
     written = 0;
 
@@ -265,6 +274,23 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
     rowlimit = rows;
   }
 
+  /**
+   * Sets the JDBC {@link Statement#setQueryTimeout(int)} (seconds) for 
statements opened by {@link
+   * #openQuery(String, IRowMeta, Object[], int, boolean)} until {@link 
#disconnect()}. Use {@code
+   * 0} to use the driver default.
+   *
+   * @param seconds query timeout in whole seconds; values {@code < 0} are 
treated as {@code 0}
+   */
+  public void setStatementQueryTimeoutSeconds(int seconds) {
+    this.statementQueryTimeoutSeconds = Math.max(0, seconds);
+  }
+
+  private void applyStatementQueryTimeout(Statement statement) throws 
SQLException {
+    if (statement != null && statementQueryTimeoutSeconds > 0) {
+      statement.setQueryTimeout(statementQueryTimeoutSeconds);
+    }
+  }
+
   /**
    * @return Returns the prepStatementInsert.
    */
@@ -610,6 +636,7 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
                 + hde.getMessage());
         log.logError(Const.getStackTracker(hde));
       }
+      statementQueryTimeoutSeconds = 0;
     }
   }
 
@@ -1591,6 +1618,8 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
           pstmt.setMaxRows(rowlimit);
         }
 
+        applyStatementQueryTimeout(pstmt);
+
         log.snap(Metrics.METRIC_DATABASE_EXECUTE_SQL_START, 
databaseMeta.getName());
         res = pstmt.executeQuery();
         log.snap(Metrics.METRIC_DATABASE_EXECUTE_SQL_STOP, 
databaseMeta.getName());
@@ -1612,6 +1641,8 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
           selStmt.setMaxRows(rowlimit);
         }
 
+        applyStatementQueryTimeout(selStmt);
+
         log.snap(Metrics.METRIC_DATABASE_EXECUTE_SQL_START, 
databaseMeta.getName());
         res = selStmt.executeQuery(databaseMeta.stripCR(sql));
         log.snap(Metrics.METRIC_DATABASE_EXECUTE_SQL_STOP, 
databaseMeta.getName());
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc
index dc1e443a67..69919de935 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/gettablenames.adoc
@@ -37,6 +37,12 @@ The Get Table Names transform gets table names from a 
database connection.
 !===
 |===
 
+== Preview
+
+The *Preview* button opens the same preview settings dialog as Table Input 
(row limit and query timeout in seconds). The JDBC statement timeout is applied 
**only in preview mode** from the Hop GUI.
+
+You can set a default for the timeout field with the 
xref:variables.adoc#_available_global_variables[`HOP_QUERY_PREVIEW_TIMEOUT`] 
application variable. See 
xref:pipeline/transforms/tableinput.adoc#_preview[Table Input: Preview] for the 
same behaviour details.
+
 == Options
 
 [options="header"]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
index 270f794cc4..62a7d14b47 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/tableinput.adoc
@@ -189,6 +189,13 @@ You can inject metadata into the following fields of the 
Table Input transform:
 - Execute for each row?
 - Limit size
 
+== Preview
+
+The *Preview* button opens a dialog where you set how many rows to fetch and a 
**query timeout** in seconds (JDBC `Statement#setQueryTimeout`).
+
+* The timeout applies **only while the pipeline is running in preview mode** 
from the Hop GUI (not during a normal pipeline run from a run configuration or 
xref:hop-run/index.adoc[Hop Run]).
+* You can set a default for the timeout field with the 
xref:variables.adoc#_available_global_variables[`HOP_QUERY_PREVIEW_TIMEOUT`] 
application variable (`0` = no default from the variable). When the variable is 
`0` or unset, the dialog still suggests `5` seconds for the initial value.
+
 == Options
 
 [options="header"]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/variables.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
index 0cf3ce66aa..fa2dbdff78 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/variables.adoc
@@ -287,6 +287,7 @@ Set to 0 to keep all rows indefinitely (default)
 |HOP_MAX_WORKFLOW_TRACKER_SIZE|5000|The maximum age (in minutes) of a log line 
while being kept internally by Hop. Set to 0 to keep all rows indefinitely 
(default)
 |HOP_PASSWORD_ENCODER_PLUGIN|Hop|Specifies the password encoder plugin to use 
by ID (Hop is the default).
 |HOP_PIPELINE_ROWSET_SIZE|-|Name of the environment variable that contains the 
size of the pipeline rowset size. This overwrites values that you set pipeline 
settings
+|HOP_QUERY_PREVIEW_TIMEOUT|0|Default JDBC statement query timeout in 
**seconds** for database query preview (for example the initial value in the 
preview settings dialog for Table Input / Get table names, and related preview 
flows). `0` means no default from this variable. Does not apply to normal 
pipeline execution for those transforms' JDBC timeouts in the same way; use the 
preview dialog to set the timeout for a given preview run.
 |HOP_PLUGIN_CLASSES||A comma delimited list of classes to scan for plugin 
annotations
 |HOP_ROWSET_GET_TIMEOUT|50|The name of the variable that optionally contains 
an alternative rowset get timeout (in ms).
 This only makes a difference for extremely short lived pipelines.
diff --git 
a/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNames.java
 
b/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNames.java
index 7d98c9d123..2c45593d21 100644
--- 
a/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNames.java
+++ 
b/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNames.java
@@ -27,6 +27,7 @@ import org.apache.hop.core.exception.HopTransformException;
 import org.apache.hop.core.exception.HopValueException;
 import org.apache.hop.core.row.RowDataUtil;
 import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.util.EnvUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
@@ -424,6 +425,16 @@ public class GetTableNames extends 
BaseTransform<GetTableNamesMeta, GetTableName
       }
 
       data.db = new Database(this, variables, databaseMeta);
+      if (getPipeline() != null && getPipeline().isPreview()) {
+        String raw =
+            getVariable(
+                Const.HOP_QUERY_PREVIEW_TIMEOUT,
+                EnvUtil.getSystemProperty(Const.HOP_QUERY_PREVIEW_TIMEOUT, 
"0"));
+        int statementQueryTimeoutSeconds = Math.max(0, 
Const.toInt(resolve(raw), 0));
+        if (statementQueryTimeoutSeconds > 0) {
+          
data.db.setStatementQueryTimeoutSeconds(statementQueryTimeoutSeconds);
+        }
+      }
       try {
         data.db.connect();
         if (isDetailed()) {
diff --git 
a/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNamesDialog.java
 
b/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNamesDialog.java
index 125839725e..8bb44961b1 100644
--- 
a/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNamesDialog.java
+++ 
b/plugins/transforms/gettablenames/src/main/java/org/apache/hop/pipeline/transforms/gettablenames/GetTableNamesDialog.java
@@ -27,8 +27,8 @@ import org.apache.hop.pipeline.Pipeline;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.PipelinePreviewFactory;
 import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.database.dialog.PreviewTableSettingsDialog;
 import org.apache.hop.ui.core.dialog.BaseDialog;
-import org.apache.hop.ui.core.dialog.EnterNumberDialog;
 import org.apache.hop.ui.core.dialog.EnterTextDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
@@ -656,43 +656,44 @@ public class GetTableNamesDialog extends 
BaseTransformDialog {
       return;
     }
 
+    int defaultRows = props.getDefaultPreviewSize();
+    PreviewTableSettingsDialog settingsDialog =
+        new PreviewTableSettingsDialog(shell, Math.max(1, defaultRows), 
variables, true);
+    PreviewTableSettingsDialog.Settings settings = settingsDialog.open();
+    if (settings == null) {
+      return;
+    }
+    int previewRows = settings.rowLimit > 0 ? settings.rowLimit : Math.max(1, 
defaultRows);
+    IVariables previewVariables = 
settingsDialog.getPreviewExecutionVariables();
+
     PipelineMeta previewMeta =
         PipelinePreviewFactory.generatePreviewPipeline(
             pipelineMeta.getMetadataProvider(), oneMeta, 
wTransformName.getText());
 
-    EnterNumberDialog numberDialog =
-        new EnterNumberDialog(
+    PipelinePreviewProgressDialog progressDialog =
+        new PipelinePreviewProgressDialog(
             shell,
-            props.getDefaultPreviewSize(),
-            BaseMessages.getString(PKG, 
"GetTableNamesDialog.PreviewSize.DialogTitle"),
-            BaseMessages.getString(PKG, 
"GetTableNamesDialog.PreviewSize.DialogMessage"));
-    int previewSize = numberDialog.open();
-    if (previewSize > 0) {
-      PipelinePreviewProgressDialog progressDialog =
-          new PipelinePreviewProgressDialog(
-              shell,
-              variables,
-              previewMeta,
-              new String[] {wTransformName.getText()},
-              new int[] {previewSize});
-      progressDialog.open();
-
-      if (!progressDialog.isCancelled()) {
-        Pipeline pipeline = progressDialog.getPipeline();
-        String loggingText = progressDialog.getLoggingText();
-
-        if (pipeline.getResult() != null && pipeline.getResult().getNrErrors() 
> 0) {
-          EnterTextDialog etd =
-              new EnterTextDialog(
-                  shell,
-                  BaseMessages.getString(PKG, "System.Dialog.Error.Title"),
-                  BaseMessages.getString(PKG, 
"GetTableNamesDialog.ErrorInPreview.DialogMessage"),
-                  loggingText,
-                  true);
-          etd.setReadOnly();
-          etd.open();
-        }
-
+            previewVariables,
+            previewMeta,
+            new String[] {wTransformName.getText()},
+            new int[] {previewRows});
+    progressDialog.open();
+
+    if (!progressDialog.isCancelled()) {
+      Pipeline pipeline = progressDialog.getPipeline();
+      String loggingText = progressDialog.getLoggingText();
+
+      if (pipeline.getResult() != null && pipeline.getResult().getNrErrors() > 
0) {
+        EnterTextDialog etd =
+            new EnterTextDialog(
+                shell,
+                BaseMessages.getString(PKG, "System.Dialog.Error.Title"),
+                BaseMessages.getString(PKG, 
"GetTableNamesDialog.ErrorInPreview.DialogMessage"),
+                loggingText,
+                true);
+        etd.setReadOnly();
+        etd.open();
+      } else {
         PreviewRowsDialog prd =
             new PreviewRowsDialog(
                 shell,
diff --git 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInput.java
 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInput.java
index 2aa42fb7f1..ece2fcfec3 100644
--- 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInput.java
+++ 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInput.java
@@ -31,6 +31,7 @@ import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.RowDataUtil;
 import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.util.EnvUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.pipeline.Pipeline;
@@ -341,6 +342,18 @@ public class TableInput extends 
BaseTransform<TableInputMeta, TableInputData> {
 
       data.db = new Database(this, this, databaseMeta);
       data.db.setQueryLimit(Const.toInt(resolve(meta.getRowLimit()), 0));
+      // Statement timeout is for transform dialog / pipeline preview only 
(Hop GUI sets preview).
+      // Normal pipeline runs use JDBC driver default (0 = no explicit timeout 
on the statement).
+      if (getPipeline() != null && getPipeline().isPreview()) {
+        String raw =
+            getVariable(
+                Const.HOP_QUERY_PREVIEW_TIMEOUT,
+                EnvUtil.getSystemProperty(Const.HOP_QUERY_PREVIEW_TIMEOUT, 
"0"));
+        int statementQueryTimeoutSeconds = Math.max(0, 
Const.toInt(resolve(raw), 0));
+        if (statementQueryTimeoutSeconds > 0) {
+          
data.db.setStatementQueryTimeoutSeconds(statementQueryTimeoutSeconds);
+        }
+      }
 
       try {
         data.db.connect();
diff --git 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
index 805d14b292..348dd33810 100644
--- 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
+++ 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
@@ -39,8 +39,8 @@ import org.apache.hop.pipeline.PipelinePreviewFactory;
 import org.apache.hop.pipeline.transform.TransformMeta;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.database.dialog.DatabaseExplorerDialog;
+import org.apache.hop.ui.core.database.dialog.PreviewTableSettingsDialog;
 import org.apache.hop.ui.core.dialog.BaseDialog;
-import org.apache.hop.ui.core.dialog.EnterNumberDialog;
 import org.apache.hop.ui.core.dialog.EnterTextDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
@@ -592,54 +592,56 @@ public class TableInputDialog extends BaseTransformDialog 
{
     TableInputMeta oneMeta = new TableInputMeta();
     getInfo(oneMeta, true);
 
-    EnterNumberDialog numberDialog =
-        new EnterNumberDialog(
+    int defaultRows = props.getDefaultPreviewSize();
+    PreviewTableSettingsDialog settingsDialog =
+        new PreviewTableSettingsDialog(shell, Math.max(1, defaultRows), 
variables, true);
+    PreviewTableSettingsDialog.Settings settings = settingsDialog.open();
+    if (settings == null) {
+      return;
+    }
+    int previewRows = settings.rowLimit > 0 ? settings.rowLimit : Math.max(1, 
defaultRows);
+    oneMeta.setRowLimit(Integer.toString(previewRows));
+
+    IVariables previewVariables = 
settingsDialog.getPreviewExecutionVariables();
+
+    PipelineMeta previewMeta =
+        PipelinePreviewFactory.generatePreviewPipeline(
+            pipelineMeta.getMetadataProvider(), oneMeta, 
wTransformName.getText());
+
+    PipelinePreviewProgressDialog progressDialog =
+        new PipelinePreviewProgressDialog(
             shell,
-            props.getDefaultPreviewSize(),
-            BaseMessages.getString(PKG, "TableInputDialog.EnterPreviewSize"),
-            BaseMessages.getString(PKG, 
"TableInputDialog.NumberOfRowsToPreview"));
-    int previewSize = numberDialog.open();
-    if (previewSize > 0) {
-      oneMeta.setRowLimit(Integer.toString(previewSize));
-      PipelineMeta previewMeta =
-          PipelinePreviewFactory.generatePreviewPipeline(
-              pipelineMeta.getMetadataProvider(), oneMeta, 
wTransformName.getText());
-
-      PipelinePreviewProgressDialog progressDialog =
-          new PipelinePreviewProgressDialog(
-              shell,
-              variables,
-              previewMeta,
-              new String[] {wTransformName.getText()},
-              new int[] {previewSize});
-      progressDialog.open();
-
-      Pipeline pipeline = progressDialog.getPipeline();
-      String loggingText = progressDialog.getLoggingText();
-
-      if (!progressDialog.isCancelled()) {
-        if (pipeline.getResult() != null && pipeline.getResult().getNrErrors() 
> 0) {
-          EnterTextDialog etd =
-              new EnterTextDialog(
-                  shell,
-                  BaseMessages.getString(PKG, 
"System.Dialog.PreviewError.Title"),
-                  BaseMessages.getString(PKG, 
"System.Dialog.PreviewError.Message"),
-                  loggingText,
-                  true);
-          etd.setReadOnly();
-          etd.open();
-        } else {
-          PreviewRowsDialog prd =
-              new PreviewRowsDialog(
-                  shell,
-                  variables,
-                  SWT.NONE,
-                  wTransformName.getText(),
-                  progressDialog.getPreviewRowsMeta(wTransformName.getText()),
-                  progressDialog.getPreviewRows(wTransformName.getText()),
-                  loggingText);
-          prd.open();
-        }
+            previewVariables,
+            previewMeta,
+            new String[] {wTransformName.getText()},
+            new int[] {previewRows});
+    progressDialog.open();
+
+    Pipeline pipeline = progressDialog.getPipeline();
+    String loggingText = progressDialog.getLoggingText();
+
+    if (!progressDialog.isCancelled()) {
+      if (pipeline.getResult() != null && pipeline.getResult().getNrErrors() > 
0) {
+        EnterTextDialog etd =
+            new EnterTextDialog(
+                shell,
+                BaseMessages.getString(PKG, 
"System.Dialog.PreviewError.Title"),
+                BaseMessages.getString(PKG, 
"System.Dialog.PreviewError.Message"),
+                loggingText,
+                true);
+        etd.setReadOnly();
+        etd.open();
+      } else {
+        PreviewRowsDialog prd =
+            new PreviewRowsDialog(
+                shell,
+                variables,
+                SWT.NONE,
+                wTransformName.getText(),
+                progressDialog.getPreviewRowsMeta(wTransformName.getText()),
+                progressDialog.getPreviewRows(wTransformName.getText()),
+                loggingText);
+        prd.open();
       }
     }
   }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java 
b/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
index 1f786ce515..a71bde7498 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/database/DatabaseMetaEditor.java
@@ -1043,7 +1043,9 @@ public class DatabaseMetaEditor extends 
MetadataEditor<DatabaseMeta> {
                 SWT.NONE,
                 manager.getVariables(),
                 meta,
-                manager.getSerializer().loadAll());
+                manager.getSerializer().loadAll(),
+                true,
+                true);
         dialog.open();
       } catch (Exception e) {
         new ErrorDialog(getShell(), "Error", "Error exploring database", e);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
index a9f5adb16d..0ba95d9808 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/DatabaseExplorerDialog.java
@@ -41,7 +41,6 @@ import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.ConstUi;
 import org.apache.hop.ui.core.PropsUi;
-import org.apache.hop.ui.core.dialog.EnterNumberDialog;
 import org.apache.hop.ui.core.dialog.EnterSelectionDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
@@ -57,6 +56,7 @@ import 
org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.FormAttachment;
 import org.eclipse.swt.layout.FormData;
 import org.eclipse.swt.layout.FormLayout;
@@ -118,7 +118,7 @@ public class DatabaseExplorerDialog extends Dialog {
   private static final String STRING_SYNONYMS =
       BaseMessages.getString(PKG, "DatabaseExplorerDialog.Synonyms.Label");
 
-  private final Shell parent;
+  private final Shell parentShell;
   private Shell shell;
   private Tree wTree;
   private TreeItem tiTree;
@@ -132,6 +132,7 @@ public class DatabaseExplorerDialog extends Dialog {
   private boolean splitSchemaAndTable;
   private String schemaName;
   private Composite buttonsComposite;
+  private Button wOk;
   private Button bPrev;
   private Button bPrevN;
   private Button bCount;
@@ -145,24 +146,24 @@ public class DatabaseExplorerDialog extends Dialog {
   private Control toolBar;
 
   public DatabaseExplorerDialog(
-      Shell parent,
+      Shell parentShell,
       int style,
       IVariables variables,
       DatabaseMeta conn,
       List<DatabaseMeta> databases) {
-    this(parent, style, variables, conn, databases, false, true);
+    this(parentShell, style, variables, conn, databases, false, true);
   }
 
   public DatabaseExplorerDialog(
-      Shell parent,
+      Shell parentShell,
       int style,
       IVariables variables,
       DatabaseMeta conn,
       List<DatabaseMeta> databases,
       boolean look,
       boolean splitSchemaAndTable) {
-    super(parent, style);
-    this.parent = parent;
+    super(parentShell, style);
+    this.parentShell = parentShell;
     this.dbMeta = conn;
     this.variables = variables;
     this.databases = databases;
@@ -188,9 +189,10 @@ public class DatabaseExplorerDialog extends Dialog {
     if (Const.isLinux()) {
       shell =
           new Shell(
-              parent, SWT.APPLICATION_MODAL | SWT.DIALOG_TRIM | SWT.RESIZE | 
SWT.MAX | SWT.MIN);
+              parentShell,
+              SWT.APPLICATION_MODAL | SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | 
SWT.MIN);
     } else {
-      shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | 
SWT.MIN);
+      shell = new Shell(parentShell, SWT.DIALOG_TRIM | SWT.RESIZE | SWT.MAX | 
SWT.MIN);
     }
     PropsUi.setLook(shell);
     shell.setImage(GuiResource.getInstance().getImageDatabase());
@@ -208,10 +210,11 @@ public class DatabaseExplorerDialog extends Dialog {
     // Main buttons at the bottom
     //
     List<Button> buttons = new ArrayList<>();
-    Button wOk = new Button(shell, SWT.PUSH);
+    wOk = new Button(shell, SWT.PUSH);
     wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
     wOk.addListener(SWT.Selection, e -> ok());
     buttons.add(wOk);
+    shell.setDefaultButton(wOk);
 
     Button wRefresh = new Button(shell, SWT.PUSH);
     wRefresh.setText(BaseMessages.getString(PKG, "System.Button.Refresh"));
@@ -256,6 +259,9 @@ public class DatabaseExplorerDialog extends Dialog {
     wTree.setLayoutData(fdTree);
 
     if (!getData()) {
+      if (shell != null && !shell.isDisposed()) {
+        shell.dispose();
+      }
       return false;
     }
 
@@ -271,6 +277,9 @@ public class DatabaseExplorerDialog extends Dialog {
         });
     shell.addListener(SWT.Close, e -> cancel());
 
+    // Prevent resizing below the complete control layout (notably right-side 
action buttons).
+    Point minSize = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+    shell.setMinimumSize(minSize);
     BaseTransformDialog.setSize(shell);
 
     shell.open();
@@ -311,7 +320,7 @@ public class DatabaseExplorerDialog extends Dialog {
         new SelectionAdapter() {
           @Override
           public void widgetSelected(SelectionEvent e) {
-            previewTable(activeSchemaTable, false);
+            previewTable(activeSchemaTable);
           }
         });
     FormData prevData = new FormData();
@@ -329,7 +338,7 @@ public class DatabaseExplorerDialog extends Dialog {
         new SelectionAdapter() {
           @Override
           public void widgetSelected(SelectionEvent e) {
-            previewTable(activeSchemaTable, true);
+            previewTable(activeSchemaTable);
           }
         });
     FormData prevNData = new FormData();
@@ -443,12 +452,13 @@ public class DatabaseExplorerDialog extends Dialog {
     FormData truncateData = new FormData();
     truncateData.left = new FormAttachment(0, 0);
     truncateData.right = new FormAttachment(100, 0);
-    truncateData.top = new FormAttachment(bSql, PropsUi.getMargin() * 7);
+    truncateData.top = new FormAttachment(bSql, PropsUi.getMargin());
     bTruncate.setLayoutData(truncateData);
 
     FormData fdComposite = new FormData();
     fdComposite.right = new FormAttachment(100, 0);
     fdComposite.top = new FormAttachment(0, toolBar.getBounds().height);
+    fdComposite.bottom = new FormAttachment(wOk, -2 * PropsUi.getMargin());
     buttonsComposite.setLayoutData(fdComposite);
   }
 
@@ -526,164 +536,181 @@ public class DatabaseExplorerDialog extends Dialog {
     GetDatabaseInfoProgressDialog gdipd =
         new GetDatabaseInfoProgressDialog(shell, variables, dbMeta);
     DatabaseMetaInformation dmi = gdipd.open();
-    if (dmi != null) {
-      // Clear the tree top entry
-      if (tiTree != null && !tiTree.isDisposed()) {
-        tiTree.dispose();
-      }
-
-      // New entry in the tree
-      tiTree = new TreeItem(wTree, SWT.NONE);
-      tiTree.setImage(GuiResource.getInstance().getImageDatabase());
-      tiTree.setText(dbMeta == null ? "" : dbMeta.getName());
-
-      // Show the catalogs...
-      Catalog[] catalogs = dmi.getCatalogs();
-      if (catalogs != null) {
-        TreeItem tiCat = new TreeItem(tiTree, SWT.NONE);
-        tiCat.setImage(GuiResource.getInstance().getImageFolder());
-        tiCat.setText(STRING_CATALOG);
-
-        for (Catalog catalog : catalogs) {
-          TreeItem newCat = new TreeItem(tiCat, SWT.NONE);
-          newCat.setImage(GuiResource.getInstance().getImageFolder());
-          newCat.setText(catalog.getCatalogName());
-
-          for (int j = 0; j < catalog.getItems().length; j++) {
-            String tableName = catalog.getItems()[j];
-
-            TreeItem ti = new TreeItem(newCat, SWT.NONE);
-            ti.setImage(GuiResource.getInstance().getImageTable());
-            ti.setText(tableName);
-          }
-        }
-      }
-
-      // The schema's
-      Schema[] schemas = dmi.getSchemas();
-      if (schemas != null) {
-        TreeItem tiSch = new TreeItem(tiTree, SWT.NONE);
-        tiSch.setImage(GuiResource.getInstance().getImageFolder());
-        tiSch.setText(STRING_SCHEMAS);
-
-        for (Schema schema : schemas) {
-          TreeItem newSch = new TreeItem(tiSch, SWT.NONE);
-          newSch.setImage(GuiResource.getInstance().getImageSchema());
-          newSch.setText(schema.getSchemaName());
+    if (dmi == null) {
+      return false;
+    }
+    disposeOldTreeRootIfPresent();
+    createDatabaseRootTreeItem();
+    addCatalogBranches(dmi);
+    addSchemaBranches(dmi);
+    TreeItem tiTab = addTablesBranch(dmi.getTables());
+    TreeItem tiView = addViewsBranch(dmi.getViews());
+    TreeItem tiSyn = addSynonymsBranch(dmi.getSynonyms());
+    selectInitiallySelectedTable(tiTab, tiView, tiSyn);
+    tiTree.setExpanded(true);
+    return true;
+  }
 
-          for (int j = 0; j < schema.getItems().length; j++) {
-            String tableName = schema.getItems()[j];
+  private void disposeOldTreeRootIfPresent() {
+    if (tiTree != null && !tiTree.isDisposed()) {
+      tiTree.dispose();
+    }
+  }
 
-            TreeItem ti = new TreeItem(newSch, SWT.NONE);
-            ti.setImage(GuiResource.getInstance().getImageTable());
-            ti.setText(tableName);
-          }
-        }
-      }
+  private void createDatabaseRootTreeItem() {
+    tiTree = new TreeItem(wTree, SWT.NONE);
+    tiTree.setImage(GuiResource.getInstance().getImageDatabase());
+    tiTree.setText(dbMeta == null ? "" : dbMeta.getName());
+  }
 
-      // The tables in general...
-      TreeItem tiTab = null;
-      String[] tabnames = dmi.getTables();
-      if (tabnames != null) {
-        tiTab = new TreeItem(tiTree, SWT.NONE);
-        tiTab.setImage(GuiResource.getInstance().getImageFolder());
-        tiTab.setText(STRING_TABLES);
-        tiTab.setExpanded(true);
-
-        for (String tabname : tabnames) {
-          TreeItem newTab = new TreeItem(tiTab, SWT.NONE);
-          newTab.setImage(GuiResource.getInstance().getImageTable());
-          newTab.setText(tabname);
-        }
+  private void addCatalogBranches(DatabaseMetaInformation dmi) {
+    Catalog[] catalogs = dmi.getCatalogs();
+    if (catalogs == null) {
+      return;
+    }
+    TreeItem tiCat = new TreeItem(tiTree, SWT.NONE);
+    tiCat.setImage(GuiResource.getInstance().getImageFolder());
+    tiCat.setText(STRING_CATALOG);
+    for (Catalog catalog : catalogs) {
+      TreeItem newCat = new TreeItem(tiCat, SWT.NONE);
+      newCat.setImage(GuiResource.getInstance().getImageFolder());
+      newCat.setText(catalog.getCatalogName());
+      for (String catalogItemName : catalog.getItems()) {
+        TreeItem ti = new TreeItem(newCat, SWT.NONE);
+        ti.setImage(GuiResource.getInstance().getImageTable());
+        ti.setText(catalogItemName);
       }
+    }
+  }
 
-      // The views...
-      TreeItem tiView = null;
-      String[] views = dmi.getViews();
-      if (views != null) {
-        tiView = new TreeItem(tiTree, SWT.NONE);
-        tiView.setImage(GuiResource.getInstance().getImageFolder());
-        tiView.setText(STRING_VIEWS);
-        for (String view : views) {
-          TreeItem newView = new TreeItem(tiView, SWT.NONE);
-          newView.setImage(GuiResource.getInstance().getImageView());
-          newView.setText(view);
-        }
+  private void addSchemaBranches(DatabaseMetaInformation dmi) {
+    Schema[] schemas = dmi.getSchemas();
+    if (schemas == null) {
+      return;
+    }
+    TreeItem tiSch = new TreeItem(tiTree, SWT.NONE);
+    tiSch.setImage(GuiResource.getInstance().getImageFolder());
+    tiSch.setText(STRING_SCHEMAS);
+    for (Schema schema : schemas) {
+      TreeItem newSch = new TreeItem(tiSch, SWT.NONE);
+      newSch.setImage(GuiResource.getInstance().getImageSchema());
+      newSch.setText(schema.getSchemaName());
+      for (String schemaItemName : schema.getItems()) {
+        TreeItem ti = new TreeItem(newSch, SWT.NONE);
+        ti.setImage(GuiResource.getInstance().getImageTable());
+        ti.setText(schemaItemName);
       }
+    }
+  }
 
-      // The synonyms
-      TreeItem tiSyn = null;
-      String[] syn = dmi.getSynonyms();
-      if (syn != null) {
-        tiSyn = new TreeItem(tiTree, SWT.NONE);
-        tiSyn.setImage(GuiResource.getInstance().getImageFolder());
-        tiSyn.setText(STRING_SYNONYMS);
-        for (String s : syn) {
-          TreeItem newSyn = new TreeItem(tiSyn, SWT.NONE);
-          newSyn.setImage(GuiResource.getInstance().getImageSynonym());
-          newSyn.setText(s);
-        }
-      }
+  private TreeItem addTablesBranch(String[] tabnames) {
+    if (tabnames == null) {
+      return null;
+    }
+    TreeItem tiTab = new TreeItem(tiTree, SWT.NONE);
+    tiTab.setImage(GuiResource.getInstance().getImageFolder());
+    tiTab.setText(STRING_TABLES);
+    tiTab.setExpanded(true);
+    for (String tabname : tabnames) {
+      TreeItem newTab = new TreeItem(tiTab, SWT.NONE);
+      newTab.setImage(GuiResource.getInstance().getImageTable());
+      newTab.setText(tabname);
+    }
+    return tiTab;
+  }
 
-      // Make sure the selected table is shown...
-      if (!StringUtils.isEmpty(selectedTable)) {
-        TreeItem ti = null;
-        if (ti == null && tiTab != null) {
-          ti = ConstUi.findTreeItem(tiTab, selectedSchema, selectedTable);
-        }
-        if (ti == null && tiView != null) {
-          ti = ConstUi.findTreeItem(tiView, selectedSchema, selectedTable);
-        }
-        if (ti == null && tiTree != null) {
-          ti = ConstUi.findTreeItem(tiTree, selectedSchema, selectedTable);
-        }
-        if (ti == null && tiSyn != null) {
-          ti = ConstUi.findTreeItem(tiSyn, selectedSchema, selectedTable);
-        }
+  private TreeItem addViewsBranch(String[] views) {
+    if (views == null) {
+      return null;
+    }
+    TreeItem tiView = new TreeItem(tiTree, SWT.NONE);
+    tiView.setImage(GuiResource.getInstance().getImageFolder());
+    tiView.setText(STRING_VIEWS);
+    for (String view : views) {
+      TreeItem newView = new TreeItem(tiView, SWT.NONE);
+      newView.setImage(GuiResource.getInstance().getImageView());
+      newView.setText(view);
+    }
+    return tiView;
+  }
 
-        if (ti != null) {
-          wTree.setSelection(new TreeItem[] {ti});
-          wTree.showSelection();
-          refreshButtons(
-              dbMeta.getQuotedSchemaTableCombination(variables, 
selectedSchema, selectedTable));
-        }
+  private TreeItem addSynonymsBranch(String[] syn) {
+    if (syn == null) {
+      return null;
+    }
+    TreeItem tiSyn = new TreeItem(tiTree, SWT.NONE);
+    tiSyn.setImage(GuiResource.getInstance().getImageFolder());
+    tiSyn.setText(STRING_SYNONYMS);
+    for (String s : syn) {
+      TreeItem newSyn = new TreeItem(tiSyn, SWT.NONE);
+      newSyn.setImage(GuiResource.getInstance().getImageSynonym());
+      newSyn.setText(s);
+    }
+    return tiSyn;
+  }
 
-        selectedTable = null;
+  private void selectInitiallySelectedTable(TreeItem tiTab, TreeItem tiView, 
TreeItem tiSyn) {
+    if (StringUtils.isEmpty(selectedTable)) {
+      return;
+    }
+    TreeItem ti = findTreeItemForInitialSelection(tiTab, tiView, tiSyn);
+    if (ti != null) {
+      wTree.setSelection(new TreeItem[] {ti});
+      wTree.showSelection();
+      if (dbMeta != null) {
+        refreshButtons(
+            dbMeta.getQuotedSchemaTableCombination(variables, selectedSchema, 
selectedTable));
       }
-
-      tiTree.setExpanded(true);
-    } else {
-      return false;
     }
+    selectedTable = null;
+  }
 
-    return true;
+  private TreeItem findTreeItemForInitialSelection(
+      TreeItem tiTab, TreeItem tiView, TreeItem tiSyn) {
+    TreeItem ti = null;
+    if (tiTab != null) {
+      ti = ConstUi.findTreeItem(tiTab, selectedSchema, selectedTable);
+    }
+    if (ti == null && tiView != null) {
+      ti = ConstUi.findTreeItem(tiView, selectedSchema, selectedTable);
+    }
+    if (ti == null) {
+      ti = ConstUi.findTreeItem(tiTree, selectedSchema, selectedTable);
+    }
+    if (ti == null && tiSyn != null) {
+      ti = ConstUi.findTreeItem(tiSyn, selectedSchema, selectedTable);
+    }
+    return ti;
   }
 
   private String getSchemaTable() {
     TreeItem[] ti = wTree.getSelection();
-    if (ti.length == 1) {
-      // Get the parent.
-      TreeItem parent = ti[0].getParentItem();
-      if (parent != null) {
-        String schemaName = parent.getText();
-        String tableName = ti[0].getText();
-
-        if (ti[0].getItemCount() == 0) // No children, only the tables 
themselves...
-        {
-          String tab = null;
-          if (schemaName.equalsIgnoreCase(STRING_TABLES)
-              || schemaName.equalsIgnoreCase(STRING_VIEWS)
-              || schemaName.equalsIgnoreCase(STRING_SYNONYMS)
-              || (schemaName != null && schemaName.isEmpty())) {
-            tab = tableName;
-          } else {
-            tab = dbMeta.getQuotedSchemaTableCombination(variables, 
schemaName, tableName);
-          }
-          return tab;
-        }
-      }
+    if (ti.length != 1) {
+      return null;
+    }
+    TreeItem parentItem = ti[0].getParentItem();
+    if (parentItem == null) {
+      return null;
+    }
+    String parentLabel = parentItem.getText();
+    String itemText = ti[0].getText();
+    if (ti[0].getItemCount() != 0) {
+      return null;
+    }
+    if (isRootTableViewSynonymOrEmptyParent(parentLabel)) {
+      return itemText;
     }
-    return null;
+    if (dbMeta == null) {
+      return null;
+    }
+    return dbMeta.getQuotedSchemaTableCombination(variables, parentLabel, 
itemText);
+  }
+
+  private static boolean isRootTableViewSynonymOrEmptyParent(String 
parentLabel) {
+    return parentLabel.equalsIgnoreCase(STRING_TABLES)
+        || parentLabel.equalsIgnoreCase(STRING_VIEWS)
+        || parentLabel.equalsIgnoreCase(STRING_SYNONYMS)
+        || parentLabel.isEmpty();
   }
 
   public void setTreeMenu() {
@@ -698,7 +725,7 @@ public class DatabaseExplorerDialog extends Dialog {
           new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-              previewTable(table, false);
+              previewTable(table);
             }
           });
       MenuItem miPrevN = new MenuItem(mTree, SWT.PUSH);
@@ -708,7 +735,7 @@ public class DatabaseExplorerDialog extends Dialog {
           new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-              previewTable(table, true);
+              previewTable(table);
             }
           });
       MenuItem miCount = new MenuItem(mTree, SWT.PUSH);
@@ -771,37 +798,42 @@ public class DatabaseExplorerDialog extends Dialog {
     }
   }
 
-  public void previewTable(String tableName, boolean asklimit) {
-    int limit = 100;
-    if (asklimit) {
-      // Ask how many lines we should preview.
-      String shellText = BaseMessages.getString(PKG, 
"DatabaseExplorerDialog.PreviewTable.Title");
-      String lineText = BaseMessages.getString(PKG, 
"DatabaseExplorerDialog.PreviewTable.Message");
-      EnterNumberDialog end = new EnterNumberDialog(shell, limit, shellText, 
lineText);
-      int samples = end.open();
-      if (samples >= 0) {
-        limit = samples;
-      }
+  public void previewTable(String qualifiedTableName) {
+    PreviewTableSettingsDialog settingsDialog =
+        new PreviewTableSettingsDialog(shell, 100, variables, false);
+    PreviewTableSettingsDialog.Settings settings = settingsDialog.open();
+    if (settings == null) {
+      return;
     }
+    int limit = settings.rowLimit;
+    int queryTimeoutSeconds = settings.queryTimeoutSeconds;
 
-    String[] tableNameParts = tableName.split("\\.");
+    String[] tableNameParts = qualifiedTableName.split("\\.");
 
     GetPreviewTableProgressDialog pd = null;
     if (schemaName == null && tableNameParts.length == 2) {
       // Table name contains both schema name and table name concatenated
       pd =
           new GetPreviewTableProgressDialog(
-              shell, variables, dbMeta, tableNameParts[0], tableNameParts[1], 
limit);
+              shell,
+              variables,
+              dbMeta,
+              tableNameParts[0],
+              tableNameParts[1],
+              limit,
+              queryTimeoutSeconds);
     } else {
-      pd = new GetPreviewTableProgressDialog(shell, variables, dbMeta, null, 
tableName, limit);
+      pd =
+          new GetPreviewTableProgressDialog(
+              shell, variables, dbMeta, null, qualifiedTableName, limit, 
queryTimeoutSeconds);
     }
 
     List<Object[]> rows = pd.open();
-    if (rows != null) // otherwise an already shown error...
-    {
+    if (pd.isPreviewSucceeded()) {
       if (!rows.isEmpty()) {
         PreviewRowsDialog prd =
-            new PreviewRowsDialog(shell, variables, SWT.NONE, tableName, 
pd.getRowMeta(), rows);
+            new PreviewRowsDialog(
+                shell, variables, SWT.NONE, qualifiedTableName, 
pd.getRowMeta(), rows);
         prd.open();
       } else {
         MessageBox mb = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
@@ -812,28 +844,27 @@ public class DatabaseExplorerDialog extends Dialog {
     }
   }
 
-  public void showTable(String tableName) {
-    String sql = dbMeta.getSqlQueryFields(tableName);
+  public void showTable(String qualifiedTableName) {
+    String sql = dbMeta.getSqlQueryFields(qualifiedTableName);
     IRowMeta result = null;
-    Database db = new Database(HopGui.getInstance().getLoggingObject(), 
variables, dbMeta);
-    try {
+    try (Database db = new Database(HopGui.getInstance().getLoggingObject(), 
variables, dbMeta)) {
       db.connect();
       result = db.getQueryFields(sql, false);
     } catch (Exception e) {
       // Do Nothing
-    } finally {
-      db.disconnect();
     }
     if (result != null) {
       TransformFieldsDialog sfd =
-          new TransformFieldsDialog(shell, variables, SWT.NONE, tableName, 
result);
+          new TransformFieldsDialog(shell, variables, SWT.NONE, 
qualifiedTableName, result);
       sfd.open();
     }
   }
 
-  public void showCount(String tableName) {
+  public void showCount(String qualifiedTableName) {
     String realTableName =
-        (tableName.contains(".") ? tableName.substring(tableName.indexOf(".") 
+ 1) : tableName);
+        (qualifiedTableName.contains(".")
+            ? qualifiedTableName.substring(qualifiedTableName.indexOf(".") + 1)
+            : qualifiedTableName);
     GetTableSizeProgressDialog pd =
         new GetTableSizeProgressDialog(shell, variables, dbMeta, 
realTableName);
     Long size = pd.open();
@@ -841,19 +872,23 @@ public class DatabaseExplorerDialog extends Dialog {
       MessageBox mb = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
       mb.setMessage(
           BaseMessages.getString(
-              PKG, "DatabaseExplorerDialog.TableSize.Message", tableName, 
size.toString()));
+              PKG,
+              "DatabaseExplorerDialog.TableSize.Message",
+              qualifiedTableName,
+              size.toString()));
       mb.setText(BaseMessages.getString(PKG, 
"DatabaseExplorerDialog.TableSize.Title"));
       mb.open();
     }
   }
 
-  public void getDDL(String tableName) {
-    Database db = new Database(loggingObject, variables, dbMeta);
-    try {
+  public void getDDL(String qualifiedTableName) {
+    try (Database db = new Database(loggingObject, variables, dbMeta)) {
       db.connect();
-      IRowMeta r = db.getTableFields(tableName);
+      IRowMeta r = db.getTableFields(qualifiedTableName);
       String realTableName =
-          (tableName.contains(".") ? 
tableName.substring(tableName.indexOf(".") + 1) : tableName);
+          (qualifiedTableName.contains(".")
+              ? qualifiedTableName.substring(qualifiedTableName.indexOf(".") + 
1)
+              : qualifiedTableName);
 
       String sql = db.getCreateTableStatement(realTableName, r, null, false, 
null, true);
       SqlEditor se = new SqlEditor(shell, SWT.NONE, variables, dbMeta, 
dbcache, sql);
@@ -864,20 +899,19 @@ public class DatabaseExplorerDialog extends Dialog {
           BaseMessages.getString(PKG, "Dialog.Error.Header"),
           BaseMessages.getString(PKG, 
"DatabaseExplorerDialog.Error.RetrieveLayout"),
           dbe);
-    } finally {
-      db.disconnect();
     }
   }
 
-  public void getDDLForOther(String tableName) {
+  public void getDDLForOther(String qualifiedTableName) {
 
     if (databases != null) {
-      Database database = new Database(loggingObject, variables, dbMeta);
-      try {
+      try (Database database = new Database(loggingObject, variables, dbMeta)) 
{
         database.connect();
 
         String realTableName =
-            (tableName.contains(".") ? 
tableName.substring(tableName.indexOf(".") + 1) : tableName);
+            (qualifiedTableName.contains(".")
+                ? qualifiedTableName.substring(qualifiedTableName.indexOf(".") 
+ 1)
+                : qualifiedTableName);
         IRowMeta rowMeta = database.getTableFields(realTableName);
 
         // Now select the other connection...
@@ -902,12 +936,14 @@ public class DatabaseExplorerDialog extends Dialog {
         String target = enterSelectionDialog.open();
         if (target != null) {
           DatabaseMeta targetDatabaseMeta = 
DatabaseMeta.findDatabase(databaseMetaList, target);
-          Database targetDatabase = new Database(loggingObject, variables, 
targetDatabaseMeta);
-
-          String sql =
-              targetDatabase.getCreateTableStatement(tableName, rowMeta, null, 
false, null, true);
-          SqlEditor sqlEditor = new SqlEditor(shell, SWT.NONE, variables, 
dbMeta, dbcache, sql);
-          sqlEditor.open();
+          try (Database targetDatabase =
+              new Database(loggingObject, variables, targetDatabaseMeta)) {
+            String sql =
+                targetDatabase.getCreateTableStatement(
+                    qualifiedTableName, rowMeta, null, false, null, true);
+            SqlEditor sqlEditor = new SqlEditor(shell, SWT.NONE, variables, 
dbMeta, dbcache, sql);
+            sqlEditor.open();
+          }
         }
       } catch (HopDatabaseException dbe) {
         new ErrorDialog(
@@ -915,8 +951,6 @@ public class DatabaseExplorerDialog extends Dialog {
             BaseMessages.getString(PKG, "Dialog.Error.Header"),
             BaseMessages.getString(PKG, "DatabaseExplorerDialog.Error.GenDDL"),
             dbe);
-      } finally {
-        database.disconnect();
       }
     } else {
       MessageBox mb = new MessageBox(shell, SWT.NONE | SWT.ICON_INFORMATION);
@@ -927,19 +961,26 @@ public class DatabaseExplorerDialog extends Dialog {
     }
   }
 
-  public void getSql(String tableName) {
+  public void getSql(String qualifiedTableName) {
     String realTableName =
-        (tableName.contains(".") ? tableName.substring(tableName.indexOf(".") 
+ 1) : tableName);
+        (qualifiedTableName.contains(".")
+            ? qualifiedTableName.substring(qualifiedTableName.indexOf(".") + 1)
+            : qualifiedTableName);
     SqlEditor sqlEditor =
         new SqlEditor(
             shell, SWT.NONE, variables, dbMeta, dbcache, "SELECT * FROM " + 
realTableName);
     sqlEditor.open();
   }
 
-  public void getTruncate(String activeSchemaTable) {
+  public void getTruncate(String truncateTableReference) {
     SqlEditor sql =
         new SqlEditor(
-            shell, SWT.NONE, variables, dbMeta, dbcache, "-- TRUNCATE TABLE " 
+ activeSchemaTable);
+            shell,
+            SWT.NONE,
+            variables,
+            dbMeta,
+            dbcache,
+            "-- TRUNCATE TABLE " + truncateTableReference);
     sql.open();
   }
 
@@ -953,35 +994,63 @@ public class DatabaseExplorerDialog extends Dialog {
       dispose();
       return;
     }
-    TreeItem[] ti = wTree.getSelection();
-    if (ti.length == 1) {
-      // Get the parent.
-      String table = ti[0].getText();
-      String[] path = ConstUi.getTreeStrings(ti[0]);
-      if (path.length == 3
-          && (STRING_TABLES.equalsIgnoreCase(path[1])
-              || STRING_VIEWS.equalsIgnoreCase(path[1])
-              || STRING_SYNONYMS.equalsIgnoreCase(path[1]))) {
-        schemaName = null;
-        tableName = table;
-        String[] st = tableName.split("\\.", 2);
-        if (st.length > 1) { // we have a dot in there and need to separate
-          schemaName = st[0];
-          tableName = st[1];
-        }
-        dispose();
-      }
-      if (path.length == 4 && (STRING_SCHEMAS.equals(path[1]) || 
STRING_CATALOG.equals(path[1]))) {
-        if (splitSchemaAndTable) {
-          schemaName = path[2];
-          tableName = path[3];
-        } else {
-          schemaName = null;
-          tableName = dbMeta.getQuotedSchemaTableCombination(variables, 
path[2], path[3]);
-        }
-        dispose();
+    TreeItem[] selection = wTree.getSelection();
+    if (selection.length != 1) {
+      return;
+    }
+    TreeItem selected = selection[0];
+    if (completeOkFromThreePartPath(selected)) {
+      return;
+    }
+    completeOkFromFourPartPath(selected);
+  }
+
+  private boolean completeOkFromThreePartPath(TreeItem selected) {
+    String[] path = ConstUi.getTreeStrings(selected);
+    if (path.length != 3 || !isTablesViewsOrSynonymsPathSegment(path[1])) {
+      return false;
+    }
+    applyOkSelectionForFlatTableFolder(selected.getText());
+    dispose();
+    return true;
+  }
+
+  private void applyOkSelectionForFlatTableFolder(String selectedItemText) {
+    schemaName = null;
+    tableName = selectedItemText;
+    String[] st = tableName.split("\\.", 2);
+    if (st.length > 1) {
+      schemaName = st[0];
+      tableName = st[1];
+    }
+  }
+
+  private void completeOkFromFourPartPath(TreeItem selected) {
+    String[] path = ConstUi.getTreeStrings(selected);
+    if (path.length != 4 || !isCatalogOrSchemasPathSegment(path[1])) {
+      return;
+    }
+    if (splitSchemaAndTable) {
+      schemaName = path[2];
+      tableName = path[3];
+    } else {
+      if (dbMeta == null) {
+        return;
       }
+      schemaName = null;
+      tableName = dbMeta.getQuotedSchemaTableCombination(variables, path[2], 
path[3]);
     }
+    dispose();
+  }
+
+  private static boolean isTablesViewsOrSynonymsPathSegment(String 
pathSegment) {
+    return STRING_TABLES.equalsIgnoreCase(pathSegment)
+        || STRING_VIEWS.equalsIgnoreCase(pathSegment)
+        || STRING_SYNONYMS.equalsIgnoreCase(pathSegment);
+  }
+
+  private static boolean isCatalogOrSchemasPathSegment(String pathSegment) {
+    return STRING_SCHEMAS.equals(pathSegment) || 
STRING_CATALOG.equals(pathSegment);
   }
 
   public void openSchema(Event e) {
@@ -997,7 +1066,7 @@ public class DatabaseExplorerDialog extends Dialog {
           if (!justLook) {
             ok();
           } else {
-            previewTable(tableName, false);
+            previewTable(tableName);
           }
         }
       }
@@ -1012,10 +1081,10 @@ public class DatabaseExplorerDialog extends Dialog {
   }
 
   /**
-   * @param schemaName the schemaName to set
+   * @param schema the schema name to set
    */
-  public void setSchemaName(String schemaName) {
-    this.schemaName = schemaName;
+  public void setSchemaName(String schema) {
+    this.schemaName = schema;
   }
 
   /**
@@ -1026,10 +1095,10 @@ public class DatabaseExplorerDialog extends Dialog {
   }
 
   /**
-   * @param tableName the tableName to set
+   * @param name the table name to set
    */
-  public void setTableName(String tableName) {
-    this.tableName = tableName;
+  public void setTableName(String name) {
+    this.tableName = name;
   }
 
   /**
@@ -1060,8 +1129,8 @@ public class DatabaseExplorerDialog extends Dialog {
     this.selectedSchema = selectSchema;
   }
 
-  public void setSelectedSchemaAndTable(String schemaName, String tableName) {
-    this.selectedSchema = schemaName;
-    this.selectedTable = tableName;
+  public void setSelectedSchemaAndTable(String schema, String table) {
+    this.selectedSchema = schema;
+    this.selectedTable = table;
   }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetDatabaseInfoProgressDialog.java
 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetDatabaseInfoProgressDialog.java
index 7421bb0a83..f60e87d5b6 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetDatabaseInfoProgressDialog.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetDatabaseInfoProgressDialog.java
@@ -101,10 +101,16 @@ public class GetDatabaseInfoProgressDialog {
    * @param e
    */
   private void showErrorDialog(Exception e) {
+    String databaseName = databaseMeta == null ? "" : databaseMeta.getName();
+    String message =
+        BaseMessages.getString(PKG, 
"GetDatabaseInfoProgressDialog.Error.Message")
+            + (databaseName.isEmpty()
+                ? ""
+                : " Unable to connect to database '" + databaseName + "'.");
     new ErrorDialog(
         shell,
         BaseMessages.getString(PKG, 
"GetDatabaseInfoProgressDialog.Error.Title"),
-        BaseMessages.getString(PKG, 
"GetDatabaseInfoProgressDialog.Error.Message"),
+        message,
         e);
   }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetPreviewTableProgressDialog.java
 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetPreviewTableProgressDialog.java
index 719871afbc..2209552a3c 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetPreviewTableProgressDialog.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/GetPreviewTableProgressDialog.java
@@ -18,13 +18,14 @@
 package org.apache.hop.ui.core.database.dialog;
 
 import java.lang.reflect.InvocationTargetException;
+import java.util.Collections;
 import java.util.List;
+import lombok.Getter;
 import org.apache.hop.core.IProgressMonitor;
 import org.apache.hop.core.IRunnableWithProgress;
 import org.apache.hop.core.ProgressMonitorAdapter;
 import org.apache.hop.core.database.Database;
 import org.apache.hop.core.database.DatabaseMeta;
-import org.apache.hop.core.exception.HopDatabaseException;
 import org.apache.hop.core.exception.HopException;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.variables.IVariables;
@@ -49,8 +50,15 @@ public class GetPreviewTableProgressDialog {
   private DatabaseMeta dbMeta;
   private String tableName;
   private int limit;
-  private List<Object[]> rows;
-  private IRowMeta rowMeta;
+
+  /** JDBC {@link java.sql.Statement#setQueryTimeout(int)} in seconds; 0 = 
driver default. */
+  private final int queryTimeoutSeconds;
+
+  private List<Object[]> rows = Collections.emptyList();
+  @Getter private IRowMeta rowMeta;
+
+  /** False when preview failed (error dialog was shown). */
+  @Getter private boolean previewSucceeded = true;
 
   private Database db;
 
@@ -61,98 +69,127 @@ public class GetPreviewTableProgressDialog {
       DatabaseMeta dbInfo,
       String schemaName,
       String tableName,
-      int limit) {
+      int limit,
+      int queryTimeoutSeconds) {
     this.shell = shell;
     this.variables = variables;
     this.dbMeta = dbInfo;
     this.tableName = dbInfo.getQuotedSchemaTableCombination(variables, 
schemaName, tableName);
     this.limit = limit;
+    this.queryTimeoutSeconds = Math.max(0, queryTimeoutSeconds);
   }
 
+  /**
+   * @return preview rows; empty when none or on failure. On failure {@link 
#previewSucceeded} is
+   *     false; callers typically use the Lombok-generated {@code 
isPreviewSucceeded()} accessor.
+   */
   public List<Object[]> open() {
+    previewSucceeded = true;
+    rows = Collections.emptyList();
     if (!EnvironmentUtils.getInstance().isWeb()) {
-      IRunnableWithProgress op =
-          monitor -> {
-            db = new Database(HopGui.getInstance().getLoggingObject(), 
variables, dbMeta);
-            try {
-              db.connect();
-
-              if (limit > 0) {
-                db.setQueryLimit(limit);
-              }
-
-              rows = db.getFirstRows(tableName, limit, new 
ProgressMonitorAdapter(monitor));
-              rowMeta = db.getReturnRowMeta();
-            } catch (HopException e) {
-              throw new InvocationTargetException(
-                  e, "Couldn't find any rows because of an error :" + 
e.toString());
-            } finally {
-              db.disconnect();
-            }
-          };
-      try {
-        final ProgressMonitorDialog pmd = new ProgressMonitorDialog(shell);
-        // Run something in the background to cancel active database queries, 
forecably if needed!
-        Runnable run =
-            () -> {
-              IProgressMonitor monitor = pmd.getProgressMonitor();
-              while (pmd.getShell() == null
-                  || (!pmd.getShell().isDisposed() && !monitor.isCanceled())) {
-                try {
-                  Thread.sleep(100);
-                } catch (InterruptedException e) {
-                  // Ignore
-                }
-              }
-
-              if (monitor.isCanceled()) { // Disconnect and see what happens!
+      openDesktop();
+    } else {
+      openWeb();
+    }
+    return rows;
+  }
 
-                try {
-                  db.cancelQuery();
-                } catch (Exception e) {
-                  // Ignore
-                }
-              }
-            };
-        // Start the cancel tracker in the background!
-        new Thread(run).start();
+  private void openDesktop() {
+    IRunnableWithProgress op = this::runPreviewWithResources;
+    try {
+      ProgressMonitorDialog pmd = new ProgressMonitorDialog(shell);
+      startCancelWatcherThread(pmd);
+      pmd.run(true, op);
+    } catch (InvocationTargetException e) {
+      previewSucceeded = false;
+      showErrorDialog(e);
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      previewSucceeded = false;
+      showErrorDialog(e);
+    }
+  }
 
-        pmd.run(true, op);
-      } catch (InvocationTargetException | InterruptedException e) {
-        showErrorDialog(e);
-        return null;
+  private void runPreviewWithResources(IProgressMonitor monitor) throws 
InvocationTargetException {
+    try (Database database =
+        new Database(HopGui.getInstance().getLoggingObject(), variables, 
dbMeta)) {
+      db = database;
+      database.connect();
+      if (queryTimeoutSeconds > 0) {
+        database.setStatementQueryTimeoutSeconds(queryTimeoutSeconds);
       }
-    } else {
-      try {
-        Cursor cursor = new Cursor(shell.getDisplay(), SWT.CURSOR_WAIT);
-        shell.setCursor(cursor);
+      if (limit > 0) {
+        database.setQueryLimit(limit);
+      }
+      rows = database.getFirstRows(tableName, limit, new 
ProgressMonitorAdapter(monitor));
+      rowMeta = database.getReturnRowMeta();
+    } catch (HopException e) {
+      throw new InvocationTargetException(
+          e, "Couldn't find any rows because of an error :" + e.toString());
+    } finally {
+      db = null;
+    }
+  }
 
-        db = new Database(HopGui.getInstance().getLoggingObject(), variables, 
dbMeta);
-        db.connect();
+  private void startCancelWatcherThread(ProgressMonitorDialog pmd) {
+    Runnable run =
+        () -> {
+          IProgressMonitor monitor = pmd.getProgressMonitor();
+          while (pmd.getShell() == null
+              || (!pmd.getShell().isDisposed() && !monitor.isCanceled())) {
+            try {
+              Thread.sleep(100);
+            } catch (InterruptedException e) {
+              Thread.currentThread().interrupt();
+              return;
+            }
+          }
+          if (monitor.isCanceled()) {
+            try {
+              if (db != null) {
+                db.cancelQuery();
+              }
+            } catch (Exception e) {
+              // Ignore
+            }
+          }
+        };
+    Thread cancelWatcher = new Thread(run, "Hop-DB-Preview-CancelWatcher");
+    cancelWatcher.setDaemon(true);
+    cancelWatcher.start();
+  }
 
+  private void openWeb() {
+    Cursor cursor = new Cursor(shell.getDisplay(), SWT.CURSOR_WAIT);
+    try {
+      shell.setCursor(cursor);
+      try (Database database =
+          new Database(HopGui.getInstance().getLoggingObject(), variables, 
dbMeta)) {
+        db = database;
+        database.connect();
+        if (queryTimeoutSeconds > 0) {
+          database.setStatementQueryTimeoutSeconds(queryTimeoutSeconds);
+        }
         if (limit > 0) {
-          db.setQueryLimit(limit);
+          database.setQueryLimit(limit);
         }
-
-        rows = db.getFirstRows(tableName, limit, null);
-        rowMeta = db.getReturnRowMeta();
-
-      } catch (HopDatabaseException e) {
-        showErrorDialog(e);
-        return null;
-      } finally {
-        db.disconnect();
+        rows = database.getFirstRows(tableName, limit, null);
+        rowMeta = database.getReturnRowMeta();
       }
+    } catch (HopException e) {
+      previewSucceeded = false;
+      showErrorDialog(e);
+    } finally {
+      if (!shell.isDisposed()) {
+        shell.setCursor(null);
+      }
+      if (!cursor.isDisposed()) {
+        cursor.dispose();
+      }
+      db = null;
     }
-
-    return rows;
   }
 
-  /**
-   * Showing an error dialog
-   *
-   * @param e
-   */
   private void showErrorDialog(Exception e) {
     new ErrorDialog(
         shell,
@@ -160,11 +197,4 @@ public class GetPreviewTableProgressDialog {
         BaseMessages.getString(PKG, 
"GetPreviewTableProgressDialog.Error.Message"),
         e);
   }
-
-  /**
-   * @return the rowMeta
-   */
-  public IRowMeta getRowMeta() {
-    return rowMeta;
-  }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/PreviewTableSettingsDialog.java
 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/PreviewTableSettingsDialog.java
new file mode 100644
index 0000000000..9ff9f9e532
--- /dev/null
+++ 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/PreviewTableSettingsDialog.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.core.database.dialog;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.util.EnvUtil;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.MessageBox;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/** Asks for row limit and JDBC query timeout before a database table preview. 
*/
+public class PreviewTableSettingsDialog extends Dialog {
+  private static final Class<?> PKG = PreviewTableSettingsDialog.class;
+
+  /** Row limit and {@link java.sql.Statement#setQueryTimeout(int)} in seconds 
(0 = no limit). */
+  public static final class Settings {
+    public final int rowLimit;
+    public final int queryTimeoutSeconds;
+
+    public Settings(int rowLimit, int queryTimeoutSeconds) {
+      this.rowLimit = rowLimit;
+      this.queryTimeoutSeconds = queryTimeoutSeconds;
+    }
+  }
+
+  private final Shell parentShell;
+  private final int initialRowLimit;
+
+  /** Hop variable space for default timeout in the dialog; may be {@code 
null}. */
+  private final IVariables hopVariables;
+
+  /**
+   * When {@code true}, {@link #open()} on OK clones {@code hopVariables} and 
sets {@code
+   * HOP_QUERY_PREVIEW_TIMEOUT} on the clone; use {@link 
#getPreviewExecutionVariables()} for
+   * pipeline preview.
+   */
+  private final boolean forTableInputPipelinePreview;
+
+  private Shell shell;
+  private Text wRows;
+  private Text wTimeout;
+  private Settings result;
+  private Variables previewExecutionVariables;
+  private final PropsUi props = PropsUi.getInstance();
+
+  /**
+   * @param hopVariables context for resolving default timeout (may be null 
unless {@code
+   *     forTableInputPipelinePreview})
+   * @param forTableInputPipelinePreview when true, {@code hopVariables} is 
required; OK builds
+   *     {@link #getPreviewExecutionVariables()}
+   */
+  public PreviewTableSettingsDialog(
+      Shell parent,
+      int initialRowLimit,
+      IVariables hopVariables,
+      boolean forTableInputPipelinePreview) {
+    super(parent, SWT.NONE);
+    this.parentShell = parent;
+    this.initialRowLimit = initialRowLimit;
+    this.hopVariables = hopVariables;
+    this.forTableInputPipelinePreview = forTableInputPipelinePreview;
+    if (forTableInputPipelinePreview && hopVariables == null) {
+      throw new IllegalArgumentException(
+          "hopVariables is required when forTableInputPipelinePreview is 
true");
+    }
+  }
+
+  private static int defaultTimeoutFieldSeconds(IVariables hopVariables) {
+    IVariables forResolve = hopVariables != null ? hopVariables : new 
Variables();
+    String raw =
+        forResolve.getVariable(
+            Const.HOP_QUERY_PREVIEW_TIMEOUT,
+            EnvUtil.getSystemProperty(Const.HOP_QUERY_PREVIEW_TIMEOUT, "20"));
+    return Math.max(0, Const.toInt(forResolve.resolve(raw), 0));
+  }
+
+  /**
+   * @return preview settings, or {@code null} if the user cancelled
+   */
+  public Settings open() {
+    result = null;
+    previewExecutionVariables = null;
+    int initialQueryTimeoutSeconds = defaultTimeoutFieldSeconds(hopVariables);
+
+    shell = new Shell(parentShell, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL | 
SWT.SHEET);
+    PropsUi.setLook(shell);
+    shell.setImage(GuiResource.getInstance().getImageHopUi());
+    shell.setText(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Title"));
+
+    FormLayout formLayout = new FormLayout();
+    formLayout.marginWidth = BaseDialog.MARGIN_SIZE;
+    formLayout.marginHeight = BaseDialog.MARGIN_SIZE;
+    shell.setLayout(formLayout);
+
+    Label wlRows = new Label(shell, SWT.LEFT);
+    wlRows.setText(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Rows.Label"));
+    PropsUi.setLook(wlRows);
+    FormData fdlRows = new FormData();
+    fdlRows.left = new FormAttachment(0, 0);
+    fdlRows.top = new FormAttachment(0, 0);
+    wlRows.setLayoutData(fdlRows);
+
+    wRows = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wRows);
+    wRows.setText(Integer.toString(initialRowLimit));
+    FormData fdRows = new FormData();
+    fdRows.left = new FormAttachment(0, 0);
+    fdRows.top = new FormAttachment(wlRows, BaseDialog.LABEL_SPACING);
+    fdRows.right = new FormAttachment(100, 0);
+    wRows.setLayoutData(fdRows);
+
+    Label wlRowsHint = new Label(shell, SWT.LEFT);
+    wlRowsHint.setText(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Rows.Hint"));
+    PropsUi.setLook(wlRowsHint);
+    FormData fdlRowsHint = new FormData();
+    fdlRowsHint.left = new FormAttachment(0, 0);
+    fdlRowsHint.top = new FormAttachment(wRows, BaseDialog.LABEL_SPACING);
+    fdlRowsHint.right = new FormAttachment(100, 0);
+    wlRowsHint.setLayoutData(fdlRowsHint);
+
+    Label wlTimeout = new Label(shell, SWT.LEFT);
+    wlTimeout.setText(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Timeout.Label"));
+    PropsUi.setLook(wlTimeout);
+    FormData fdlTimeout = new FormData();
+    fdlTimeout.left = new FormAttachment(0, 0);
+    fdlTimeout.top = new FormAttachment(wlRowsHint, 
BaseDialog.ELEMENT_SPACING);
+    wlTimeout.setLayoutData(fdlTimeout);
+
+    wTimeout = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wTimeout);
+    wTimeout.setText(Integer.toString(initialQueryTimeoutSeconds));
+    FormData fdTimeout = new FormData();
+    fdTimeout.left = new FormAttachment(0, 0);
+    fdTimeout.top = new FormAttachment(wlTimeout, BaseDialog.LABEL_SPACING);
+    fdTimeout.right = new FormAttachment(100, 0);
+    wTimeout.setLayoutData(fdTimeout);
+
+    Label wlTimeoutHint = new Label(shell, SWT.LEFT);
+    wlTimeoutHint.setText(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Timeout.Hint"));
+    PropsUi.setLook(wlTimeoutHint);
+    FormData fdlTimeoutHint = new FormData();
+    fdlTimeoutHint.left = new FormAttachment(0, 0);
+    fdlTimeoutHint.top = new FormAttachment(wTimeout, 
BaseDialog.LABEL_SPACING);
+    fdlTimeoutHint.right = new FormAttachment(100, 0);
+    wlTimeoutHint.setLayoutData(fdlTimeoutHint);
+
+    Button wOk = new Button(shell, SWT.PUSH);
+    wOk.setText(BaseMessages.getString("System.Button.OK"));
+    Button wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString("System.Button.Cancel"));
+    BaseTransformDialog.positionBottomButtons(
+        shell, new Button[] {wOk, wCancel}, PropsUi.getMargin(), 
wlTimeoutHint);
+
+    wOk.addListener(SWT.Selection, e -> ok());
+    wCancel.addListener(SWT.Selection, e -> cancel());
+    shell.setDefaultButton(wOk);
+
+    wRows.setFocus();
+    wRows.selectAll();
+
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel(), false);
+    return result;
+  }
+
+  /**
+   * After a successful {@link #open()} with {@code 
forTableInputPipelinePreview}, returns a clone
+   * of the constructor {@code hopVariables} with {@code 
HOP_QUERY_PREVIEW_TIMEOUT} set from the
+   * dialog. Otherwise {@code null}.
+   */
+  public Variables getPreviewExecutionVariables() {
+    return previewExecutionVariables;
+  }
+
+  private void ok() {
+    try {
+      int rows = Integer.parseInt(wRows.getText().trim());
+      int timeout = Integer.parseInt(wTimeout.getText().trim());
+      result = new Settings(Math.max(0, rows), Math.max(0, timeout));
+      if (forTableInputPipelinePreview) {
+        previewExecutionVariables = new Variables();
+        previewExecutionVariables.copyFrom(hopVariables);
+        previewExecutionVariables.setVariable(
+            Const.HOP_QUERY_PREVIEW_TIMEOUT, 
Integer.toString(result.queryTimeoutSeconds));
+      }
+      dispose();
+    } catch (NumberFormatException e) {
+      MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+      mb.setMessage(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Error.InvalidNumber"));
+      mb.setText(BaseMessages.getString(PKG, 
"PreviewTableSettingsDialog.Error.Title"));
+      mb.open();
+    }
+  }
+
+  private void cancel() {
+    result = null;
+    previewExecutionVariables = null;
+    dispose();
+  }
+
+  private void dispose() {
+    props.setScreen(new WindowProperty(shell));
+    shell.dispose();
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java
index 67e475f344..e37639050d 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java
@@ -17,9 +17,12 @@
 
 package org.apache.hop.ui.core.database.dialog;
 
+import java.lang.reflect.InvocationTargetException;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.DbCache;
+import org.apache.hop.core.IProgressMonitor;
 import org.apache.hop.core.Props;
 import org.apache.hop.core.database.Database;
 import org.apache.hop.core.database.DatabaseMeta;
@@ -32,6 +35,7 @@ import org.apache.hop.core.logging.LogChannel;
 import org.apache.hop.core.logging.LoggingObjectType;
 import org.apache.hop.core.logging.SimpleLoggingObject;
 import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.util.EnvUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.i18n.BaseMessages;
@@ -41,6 +45,7 @@ import org.apache.hop.ui.core.dialog.EnterTextDialog;
 import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
+import org.apache.hop.ui.core.dialog.ProgressMonitorDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.core.widget.SQLStyledTextComp;
@@ -86,6 +91,12 @@ public class SqlEditor {
 
   private final IVariables variables;
 
+  /**
+   * Holds the active database connection so the cancel watcher can call 
cancelQuery(). Use {@link
+   * AtomicReference} for safe publication between the worker and 
cancel-watcher threads.
+   */
+  private final AtomicReference<Database> activeDb = new AtomicReference<>();
+
   public SqlEditor(
       Shell parent, int style, IVariables variables, DatabaseMeta ci, DbCache 
dbc, String sql) {
     props = PropsUi.getInstance();
@@ -289,120 +300,19 @@ public class SqlEditor {
       return;
     }
 
+    String sqlScript =
+        Utils.isEmpty(wScript.getSelectionText()) ? wScript.getText() : 
wScript.getSelectionText();
     StringBuilder message = new StringBuilder();
+    ProgressMonitorDialog pmd = new ProgressMonitorDialog(shell);
 
-    try (Database db = new Database(loggingObject, variables, databaseMeta)) {
-      db.connect();
-      String sqlScript =
-          Utils.isEmpty(wScript.getSelectionText())
-              ? wScript.getText()
-              : wScript.getSelectionText();
-
-      // Multiple statements in the script need to be split into individual
-      // executable statements
-      List<SqlScriptStatement> statements =
-          databaseMeta.getIDatabase().getSqlScriptStatements(sqlScript + 
Const.CR);
-
-      int nrstats = 0;
-      for (SqlScriptStatement sql : statements) {
-        if (sql.isQuery()) {
-          // A Query
-          log.logDetailed("launch SELECT statement: " + Const.CR + sql);
-
-          nrstats++;
-          try {
-            List<Object[]> rows = db.getRows(sql.getStatement(), 1000);
-            IRowMeta rowMeta = db.getReturnRowMeta();
-            if (!rows.isEmpty()) {
-              PreviewRowsDialog prd =
-                  new PreviewRowsDialog(
-                      shell,
-                      variables,
-                      SWT.NONE,
-                      BaseMessages.getString(
-                          PKG, "SQLEditor.ResultRows.Title", 
Integer.toString(nrstats)),
-                      rowMeta,
-                      rows);
-              prd.open();
-            } else {
-              MessageBox mb = new MessageBox(shell, SWT.ICON_INFORMATION | 
SWT.OK);
-              mb.setMessage(BaseMessages.getString(PKG, 
"SQLEditor.NoRows.Message", sql));
-              mb.setText(BaseMessages.getString(PKG, 
"SQLEditor.NoRows.Title"));
-              mb.open();
-            }
-          } catch (HopDatabaseException dbe) {
-            new ErrorDialog(
-                shell,
-                BaseMessages.getString(PKG, "SQLEditor.ErrorExecSQL.Title"),
-                BaseMessages.getString(PKG, "SQLEditor.ErrorExecSQL.Message", 
sql),
-                dbe);
-          }
-        } else {
-          log.logDetailed("launch DDL statement: " + Const.CR + sql);
+    startSqlCancelWatcherVirtualThread(pmd);
 
-          // A DDL statement
-          nrstats++;
-          int startLogLine = HopLogStore.getLastBufferLineNr();
-          try {
-
-            log.logDetailed("Executing SQL: " + Const.CR + sql);
-            db.execStatement(sql.getStatement());
-
-            message.append(BaseMessages.getString(PKG, 
"SQLEditor.Log.SQLExecuted", sql));
-            message.append(Const.CR);
-
-            // Clear the database cache, in case we're using one...
-            if (dbcache != null) {
-              dbcache.clear(databaseMeta.getName());
-            }
-
-            // mark the statement in green in the dialog...
-            //
-            sql.setOk(true);
-          } catch (Exception dbe) {
-            sql.setOk(false);
-            String error =
-                BaseMessages.getString(PKG, "SQLEditor.Log.SQLExecError", sql, 
dbe.toString());
-            message.append(error).append(Const.CR);
-            ErrorDialog dialog =
-                new ErrorDialog(
-                    shell,
-                    BaseMessages.getString(PKG, 
"SQLEditor.ErrorExecSQL.Title"),
-                    error,
-                    dbe,
-                    true);
-            if (dialog.isCancelled()) {
-              break;
-            }
-          } finally {
-            int endLogLine = HopLogStore.getLastBufferLineNr();
-            sql.setLoggingText(
-                HopLogStore.getAppender()
-                    .getLogBufferFromTo(db.getLogChannelId(), true, 
startLogLine, endLogLine)
-                    .toString());
-            sql.setComplete(true);
-            refreshExecutionResults();
-          }
-        }
+    try {
+      pmd.run(true, monitor -> runSqlScriptWithMonitor(monitor, databaseMeta, 
sqlScript, message));
+    } catch (InterruptedException | InvocationTargetException e) {
+      if (e instanceof InterruptedException) {
+        Thread.currentThread().interrupt();
       }
-      message.append(
-          BaseMessages.getString(PKG, "SQLEditor.Log.StatsExecuted", 
Integer.toString(nrstats)));
-
-      message.append(Const.CR);
-    } catch (HopDatabaseException dbe) {
-      MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
-      String error =
-          BaseMessages.getString(
-              PKG,
-              "SQLEditor.Error.CouldNotConnect.Message",
-              (connection == null ? "" : connection.getName()),
-              dbe.getMessage());
-      message.append(error).append(Const.CR);
-      mb.setMessage(error);
-      mb.setText(BaseMessages.getString(PKG, 
"SQLEditor.Error.CouldNotConnect.Title"));
-      mb.open();
-    } finally {
-      refreshExecutionResults();
     }
 
     EnterTextDialog dialog =
@@ -415,6 +325,207 @@ public class SqlEditor {
     dialog.open();
   }
 
+  private void startSqlCancelWatcherVirtualThread(ProgressMonitorDialog pmd) {
+    Thread.ofVirtual()
+        .name("Hop-SqlEditor-CancelWatcher")
+        .start(
+            () -> {
+              IProgressMonitor monitor = pmd.getProgressMonitor();
+              while (pmd.getShell() == null
+                  || (!pmd.getShell().isDisposed() && !monitor.isCanceled())) {
+                try {
+                  Thread.sleep(100);
+                } catch (InterruptedException e) {
+                  Thread.currentThread().interrupt();
+                  break;
+                }
+              }
+              if (monitor.isCanceled()) {
+                Database db = activeDb.get();
+                if (db != null) {
+                  try {
+                    db.cancelQuery();
+                  } catch (Exception ignored) {
+                    // ignore
+                  }
+                }
+              }
+            });
+  }
+
+  private void runSqlScriptWithMonitor(
+      IProgressMonitor monitor,
+      DatabaseMeta databaseMeta,
+      String sqlScript,
+      StringBuilder message) {
+    monitor.beginTask(BaseMessages.getString(PKG, 
"SQLEditor.Monitor.Execute"), 100);
+    String raw =
+        variables.getVariable(
+            Const.HOP_QUERY_PREVIEW_TIMEOUT,
+            EnvUtil.getSystemProperty(Const.HOP_QUERY_PREVIEW_TIMEOUT, "0"));
+    int timeoutSeconds = Math.max(0, Const.toInt(variables.resolve(raw), 0));
+    try (Database db = new Database(loggingObject, variables, databaseMeta)) {
+      activeDb.set(db);
+      if (timeoutSeconds > 0) {
+        db.setStatementQueryTimeoutSeconds(timeoutSeconds);
+      }
+      db.connect();
+
+      int[] nrStats = {0};
+      runScriptStatementsLoop(db, databaseMeta, sqlScript, message, monitor, 
nrStats);
+      message.append(
+          BaseMessages.getString(PKG, "SQLEditor.Log.StatsExecuted", 
Integer.toString(nrStats[0])));
+      message.append(Const.CR);
+    } catch (HopDatabaseException dbe) {
+      handleConnectFailure(dbe, message);
+    } finally {
+      activeDb.set(null);
+      shell.getDisplay().asyncExec(this::refreshExecutionResults);
+      monitor.done();
+    }
+  }
+
+  private void runScriptStatementsLoop(
+      Database db,
+      DatabaseMeta databaseMeta,
+      String sqlScript,
+      StringBuilder message,
+      IProgressMonitor monitor,
+      int[] nrStats) {
+    List<SqlScriptStatement> statements =
+        databaseMeta.getIDatabase().getSqlScriptStatements(sqlScript + 
Const.CR);
+    boolean aborted = false;
+    for (SqlScriptStatement sql : statements) {
+      if (aborted || monitor.isCanceled()) {
+        break;
+      }
+      if (sql.isQuery()) {
+        nrStats[0]++;
+        executeSelectStatement(db, sql, nrStats[0], message);
+      } else {
+        nrStats[0]++;
+        aborted = executeDdlStatement(db, sql, databaseMeta, message);
+      }
+    }
+  }
+
+  private void executeSelectStatement(
+      Database db, SqlScriptStatement sql, int statNr, StringBuilder message) {
+    log.logDetailed("launch SELECT statement: " + Const.CR + sql);
+    try {
+      List<Object[]> rows = db.getRows(sql.getStatement(), 1000);
+      IRowMeta rowMeta = db.getReturnRowMeta();
+      if (!rows.isEmpty()) {
+        shell
+            .getDisplay()
+            .syncExec(
+                () ->
+                    new PreviewRowsDialog(
+                            shell,
+                            variables,
+                            SWT.NONE,
+                            BaseMessages.getString(
+                                PKG, "SQLEditor.ResultRows.Title", 
Integer.toString(statNr)),
+                            rowMeta,
+                            rows)
+                        .open());
+      } else {
+        shell
+            .getDisplay()
+            .syncExec(
+                () -> {
+                  MessageBox mb = new MessageBox(shell, SWT.ICON_INFORMATION | 
SWT.OK);
+                  mb.setMessage(BaseMessages.getString(PKG, 
"SQLEditor.NoRows.Message", sql));
+                  mb.setText(BaseMessages.getString(PKG, 
"SQLEditor.NoRows.Title"));
+                  mb.open();
+                });
+      }
+    } catch (HopDatabaseException dbe) {
+      handleSelectExecutionFailure(dbe, sql, message);
+    }
+  }
+
+  private void handleSelectExecutionFailure(
+      HopDatabaseException dbe, SqlScriptStatement sql, StringBuilder message) 
{
+    shell
+        .getDisplay()
+        .syncExec(
+            () ->
+                new ErrorDialog(
+                    shell,
+                    BaseMessages.getString(PKG, 
"SQLEditor.ErrorExecSQL.Title"),
+                    BaseMessages.getString(PKG, 
"SQLEditor.ErrorExecSQL.Message", sql),
+                    dbe));
+  }
+
+  private boolean executeDdlStatement(
+      Database db, SqlScriptStatement sql, DatabaseMeta databaseMeta, 
StringBuilder message) {
+    log.logDetailed("launch DDL statement: " + Const.CR + sql);
+    int startLogLine = HopLogStore.getLastBufferLineNr();
+    try {
+      log.logDetailed("Executing SQL: " + Const.CR + sql);
+      db.execStatement(sql.getStatement());
+      message.append(BaseMessages.getString(PKG, "SQLEditor.Log.SQLExecuted", 
sql));
+      message.append(Const.CR);
+      if (dbcache != null) {
+        dbcache.clear(databaseMeta.getName());
+      }
+      sql.setOk(true);
+      return false;
+    } catch (Exception dbe) {
+      return handleDdlExecutionFailure(dbe, sql, message);
+    } finally {
+      int endLogLine = HopLogStore.getLastBufferLineNr();
+      sql.setLoggingText(
+          HopLogStore.getAppender()
+              .getLogBufferFromTo(db.getLogChannelId(), true, startLogLine, 
endLogLine)
+              .toString());
+      sql.setComplete(true);
+      shell.getDisplay().asyncExec(this::refreshExecutionResults);
+    }
+  }
+
+  private boolean handleDdlExecutionFailure(
+      Exception dbe, SqlScriptStatement sql, StringBuilder message) {
+    sql.setOk(false);
+    String error = BaseMessages.getString(PKG, "SQLEditor.Log.SQLExecError", 
sql, dbe.toString());
+    message.append(error).append(Const.CR);
+    boolean[] cancelLoop = {false};
+    shell
+        .getDisplay()
+        .syncExec(
+            () -> {
+              ErrorDialog dialog =
+                  new ErrorDialog(
+                      shell,
+                      BaseMessages.getString(PKG, 
"SQLEditor.ErrorExecSQL.Title"),
+                      error,
+                      dbe,
+                      true);
+              cancelLoop[0] = dialog.isCancelled();
+            });
+    return cancelLoop[0];
+  }
+
+  private void handleConnectFailure(HopDatabaseException dbe, StringBuilder 
message) {
+    String error =
+        BaseMessages.getString(
+            PKG,
+            "SQLEditor.Error.CouldNotConnect.Message",
+            (connection == null ? "" : connection.getName()),
+            dbe.getMessage());
+    message.append(error).append(Const.CR);
+    shell
+        .getDisplay()
+        .syncExec(
+            () -> {
+              MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_ERROR);
+              mb.setMessage(error);
+              mb.setText(BaseMessages.getString(PKG, 
"SQLEditor.Error.CouldNotConnect.Title"));
+              mb.open();
+            });
+  }
+
   /** During or after an execution we will mark regions of the SQL editor 
dialog in green or red. */
   protected void refreshExecutionResults() {
     wScript.redraw();
diff --git a/ui/src/main/java/org/apache/hop/ui/core/dialog/BaseDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/BaseDialog.java
index 4543ec9710..9a8e38eafb 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/BaseDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/BaseDialog.java
@@ -57,6 +57,7 @@ import org.eclipse.swt.custom.CTabFolder;
 import org.eclipse.swt.custom.CTabItem;
 import org.eclipse.swt.events.ShellAdapter;
 import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Combo;
@@ -748,17 +749,41 @@ public abstract class BaseDialog extends Dialog {
    */
   public static void defaultShellHandling(
       Shell shell, Consumer<Void> okConsumer, Consumer<Void> cancelConsumer) {
+    defaultShellHandling(shell, okConsumer, cancelConsumer, true);
+  }
+
+  /**
+   * Like {@link #defaultShellHandling(Shell, Consumer, Consumer)} but 
controls minimum shell size.
+   *
+   * @param useStandardMinimumSize when {@code true}, keeps the legacy minimum 
(650x250) suited to
+   *     large editor dialogs; when {@code false}, minimum size follows the 
laid-out content so
+   *     small prompts are not stretched with empty space.
+   */
+  public static void defaultShellHandling(
+      Shell shell,
+      Consumer<Void> okConsumer,
+      Consumer<Void> cancelConsumer,
+      boolean useStandardMinimumSize) {
     defaultShellHandling(
         shell,
         okConsumer,
         () -> {
           cancelConsumer.accept(null);
           return true;
-        });
+        },
+        useStandardMinimumSize);
   }
 
   public static void defaultShellHandling(
       Shell shell, Consumer<Void> okConsumer, Supplier<Boolean> 
cancelSupplier) {
+    defaultShellHandling(shell, okConsumer, cancelSupplier, true);
+  }
+
+  public static void defaultShellHandling(
+      Shell shell,
+      Consumer<Void> okConsumer,
+      Supplier<Boolean> cancelSupplier,
+      boolean useStandardMinimumSize) {
 
     // If the shell is closed, cancel the dialog
     //
@@ -783,7 +808,13 @@ public abstract class BaseDialog extends Dialog {
     //
     addSpacesOnTabs(shell);
 
-    shell.setMinimumSize(650, 250);
+    if (useStandardMinimumSize) {
+      shell.setMinimumSize(650, 250);
+    } else {
+      shell.layout(true, true);
+      Point natural = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+      shell.setMinimumSize(Math.max(1, natural.x), Math.max(1, natural.y));
+    }
 
     // Set the size as well...
     //
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterNumberDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterNumberDialog.java
index d192a691f4..f30606417c 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterNumberDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/EnterNumberDialog.java
@@ -18,13 +18,12 @@
 package org.apache.hop.ui.core.dialog;
 
 import org.apache.commons.lang3.StringUtils;
-import org.apache.hop.core.Const;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.ConstUi;
-import org.apache.hop.ui.core.FormDataBuilder;
 import org.apache.hop.ui.core.PropsUi;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.MouseAdapter;
 import org.eclipse.swt.events.MouseEvent;
@@ -187,32 +186,30 @@ public class EnterNumberDialog extends Dialog {
       lastControl = wlFrom;
     }
 
-    // Some buttons
+    // Buttons aligned to the bottom of the shell
+    //
     wOk = new Button(shell, SWT.PUSH);
-    wOk.setText(BaseMessages.getString(PKG, "System.Button.OK"));
+    wOk.setText(BaseMessages.getString("System.Button.OK"));
     if (!hideCancelButton) {
       wCancel = new Button(shell, SWT.PUSH);
-      wCancel.setText(BaseMessages.getString(PKG, "System.Button.Cancel"));
+      wCancel.setText(BaseMessages.getString("System.Button.Cancel"));
+      BaseTransformDialog.positionBottomButtons(
+          shell, new Button[] {wOk, wCancel}, PropsUi.getMargin(), 
lastControl);
+    } else {
+      wCancel = null;
+      BaseTransformDialog.positionBottomButtons(
+          shell, new Button[] {wOk}, PropsUi.getMargin(), lastControl);
     }
 
-    wCancel.setLayoutData(
-        new FormDataBuilder()
-            .top(lastControl, BaseDialog.ELEMENT_SPACING * 2)
-            .right(100, 0)
-            .result());
-    wOk.setLayoutData(
-        new FormDataBuilder()
-            .top(lastControl, BaseDialog.ELEMENT_SPACING * 2)
-            .right(wCancel, Const.isOSX() ? 0 : -BaseDialog.LABEL_SPACING)
-            .result());
-
-    // Add listeners
     wOk.addListener(SWT.Selection, e -> ok());
-    wCancel.addListener(SWT.Selection, e -> cancel());
+    if (wCancel != null) {
+      wCancel.addListener(SWT.Selection, e -> cancel());
+    }
+    shell.setDefaultButton(wOk);
 
     getData();
 
-    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel());
+    BaseDialog.defaultShellHandling(shell, c -> ok(), c -> cancel(), false);
   }
 
   public void dispose() {
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java
index 0441438820..6f9fdbd6f2 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/PreviewRowsDialog.java
@@ -154,6 +154,24 @@ public class PreviewRowsDialog {
   }
 
   public void open() {
+    if (title == null) {
+      title = BaseMessages.getString(PKG, "PreviewRowsDialog.Title");
+    }
+    if (message == null) {
+      message = BaseMessages.getString(PKG, "PreviewRowsDialog.Header", 
transformName);
+    }
+
+    // Empty static preview: show a single modal on the real parent. Parenting 
a message to the
+    // preview shell here caused odd modality (that shell is created but never 
opened).
+    if (!dynamic && Utils.isEmpty(buffer)) {
+      MessageBox mb =
+          new MessageBox(parentShell, SWT.OK | SWT.ICON_INFORMATION | 
SWT.APPLICATION_MODAL);
+      mb.setText(title);
+      mb.setMessage(BaseMessages.getString(PKG, 
"PreviewRowsDialog.NoRows.Message"));
+      mb.open();
+      return;
+    }
+
     shell = new Shell(parentShell, style);
     PropsUi props = PropsUi.getInstance();
 
@@ -164,13 +182,6 @@ public class PreviewRowsDialog {
     formLayout.marginWidth = PropsUi.getFormMargin();
     formLayout.marginHeight = PropsUi.getFormMargin();
 
-    if (title == null) {
-      title = BaseMessages.getString(PKG, "PreviewRowsDialog.Title");
-    }
-    if (message == null) {
-      message = BaseMessages.getString(PKG, "PreviewRowsDialog.Header", 
transformName);
-    }
-
     if (buffer != null) {
       message += " " + BaseMessages.getString(PKG, "PreviewRowsDialog.NrRows", 
"" + buffer.size());
     }
@@ -275,19 +286,6 @@ public class PreviewRowsDialog {
       rowMeta.addValueMeta(new ValueMetaString("<waiting for rows>"));
       waitingForRows = true;
     }
-    if (!dynamic && Utils.isEmpty(buffer)) {
-      // Mmm, if we don't get any rows in the buffer: show a dialog box.
-      ShowMessageDialog dialog =
-          new ShowMessageDialog(
-              shell,
-              SWT.OK | SWT.ICON_WARNING,
-              BaseMessages.getString(PKG, "PreviewRowsDialog.NoRows.Text"),
-              BaseMessages.getString(PKG, "PreviewRowsDialog.NoRows.Message"));
-      dialog.open();
-      shell.dispose();
-      return true;
-    }
-
     ColumnInfo[] columns = new ColumnInfo[rowMeta.size()];
     for (int i = 0; i < rowMeta.size(); i++) {
       IValueMeta valueMeta = rowMeta.getValueMeta(i);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/ProgressMonitorDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/ProgressMonitorDialog.java
index 153353ef03..865955ba30 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/ProgressMonitorDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/ProgressMonitorDialog.java
@@ -18,6 +18,7 @@
 package org.apache.hop.ui.core.dialog;
 
 import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.atomic.AtomicBoolean;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.IProgressMonitor;
 import org.apache.hop.core.IRunnableWithProgress;
@@ -69,15 +70,29 @@ public class ProgressMonitorDialog {
 
   public void run(boolean cancelable, IRunnableWithProgress runnable)
       throws InvocationTargetException, InterruptedException {
+    createModalShell(cancelable);
+    int margin = layoutProgressWidgets();
+    addCancelButtonIfNeeded(cancelable, margin);
+    BaseTransformDialog.setSize(shell);
+    addShellCloseHandler();
+    shell.open();
 
-    PropsUi props = PropsUi.getInstance();
+    Cursor oldCursor = shell.getCursor();
+    parent.setCursor(display.getSystemCursor(SWT.CURSOR_WAIT));
+    try {
+      scheduleBackgroundWork(runnable);
+      pumpDisplayUntilShellDisposed();
+    } finally {
+      parent.setCursor(oldCursor);
+    }
+  }
 
+  private void createModalShell(boolean cancelable) {
     shell =
         new Shell(parent, SWT.RESIZE | SWT.APPLICATION_MODAL | (cancelable ? 
SWT.CLOSE : SWT.NONE));
     shell.setText(BaseMessages.getString(PKG, 
"ProgressMonitorDialog.Shell.Title"));
     shell.setImage(GuiResource.getInstance().getImageHopUi());
     PropsUi.setLook(shell);
-
     display = shell.getDisplay();
 
     FormLayout formLayout = new FormLayout();
@@ -85,14 +100,12 @@ public class ProgressMonitorDialog {
     formLayout.marginLeft = PropsUi.getFormMargin();
     formLayout.marginRight = PropsUi.getFormMargin();
     formLayout.marginBottom = PropsUi.getFormMargin();
+    shell.setLayout(formLayout);
+  }
 
+  private int layoutProgressWidgets() {
     int margin = PropsUi.getMargin();
 
-    shell.setLayout(formLayout);
-
-    // An image at the top right...
-    // TODO: rotate this image somehow
-    //
     Label wlImage = new Label(shell, SWT.NONE);
     wlImage.setImage(GuiResource.getInstance().getImageHopUi());
     PropsUi.setLook(wlImage);
@@ -129,69 +142,69 @@ public class ProgressMonitorDialog {
     fdProgressBar.top = new FormAttachment(wlSubTask, margin);
     wProgressBar.setLayoutData(fdProgressBar);
 
-    if (cancelable) {
-      Button wCancel = new Button(shell, SWT.PUSH);
-      wCancel.setText(BaseMessages.getString("System.Button.Cancel"));
-      wCancel.addListener(SWT.Selection, e -> isCancelled = true);
-      BaseTransformDialog.positionBottomButtons(
-          shell, new Button[] {wCancel}, margin, wProgressBar);
-    }
+    return margin;
+  }
 
-    BaseTransformDialog.setSize(shell);
+  private void addCancelButtonIfNeeded(boolean cancelable, int margin) {
+    if (!cancelable) {
+      return;
+    }
+    Button wCancel = new Button(shell, SWT.PUSH);
+    wCancel.setText(BaseMessages.getString("System.Button.Cancel"));
+    wCancel.addListener(
+        SWT.Selection,
+        e -> {
+          isCancelled = true;
+          if (display != null && !display.isDisposed()) {
+            display.wake();
+          }
+        });
+    BaseTransformDialog.positionBottomButtons(shell, new Button[] {wCancel}, 
margin, wProgressBar);
+  }
 
+  private void addShellCloseHandler() {
     shell.addListener(
         SWT.Close,
         e -> {
           e.doit = false;
           isCancelled = true;
+          if (display != null && !display.isDisposed()) {
+            display.wake();
+          }
         });
+  }
 
-    shell.open();
-
-    Cursor oldCursor = shell.getCursor();
-
-    parent.setCursor(display.getSystemCursor(SWT.CURSOR_WAIT));
+  private void scheduleBackgroundWork(IRunnableWithProgress runnable) {
+    Runnable launch =
+        () -> new Thread(() -> runMonitoredWork(runnable), 
"Hop-ProgressMonitor-Worker").start();
+    display.asyncExec(launch);
+  }
 
-    // Execute the long running task
-    //
-    Runnable longRunnable =
-        () ->
-            // Always do the work in a different thread...
-            // This keeps the shell updating properly as long as
-            // display.asyncExec is used
-            //
-            new Thread(
-                    () -> {
-                      try {
-                        runnable.run(progressMonitor);
-                      } catch (InvocationTargetException e) {
-                        targetException = e;
-                      } catch (InterruptedException e) {
-                        interruptedException = e;
-                      }
-                    })
-                .start();
-    display.asyncExec(longRunnable);
-
-    // Handle the event loop until we're done with this shell...
-    //
+  private void runMonitoredWork(IRunnableWithProgress runnable) {
     try {
-      while (!shell.isDisposed()) {
-        if (interruptedException != null) {
-          dispose();
-          throw interruptedException;
-        }
-        if (targetException != null) {
-          dispose();
-          throw targetException;
-        }
-
-        if (!display.readAndDispatch()) {
-          display.sleep();
-        }
+      runnable.run(progressMonitor);
+    } catch (InvocationTargetException e) {
+      targetException = e;
+    } catch (InterruptedException e) {
+      interruptedException = e;
+      Thread.currentThread().interrupt();
+    }
+  }
+
+  private void pumpDisplayUntilShellDisposed()
+      throws InvocationTargetException, InterruptedException {
+    while (!shell.isDisposed()) {
+      if (interruptedException != null) {
+        dispose();
+        throw interruptedException;
+      }
+      if (targetException != null) {
+        dispose();
+        throw targetException;
+      }
+      if (!display.readAndDispatch()) {
+        display.sleep();
       }
-    } finally {
-      parent.setCursor(oldCursor);
     }
   }
 
@@ -200,17 +213,59 @@ public class ProgressMonitorDialog {
   }
 
   private void dispose() {
-    display.asyncExec(
+    if (display == null || display.isDisposed()) {
+      return;
+    }
+
+    Runnable disposeRunnable =
         () -> {
-          PropsUi.getInstance().setScreen(new WindowProperty(shell));
-          shell.dispose();
-        });
+          if (shell == null || shell.isDisposed()) {
+            return;
+          }
+          try {
+            PropsUi.getInstance().setScreen(new WindowProperty(shell));
+          } catch (Exception e) {
+            // Ignore race condition when shell is already being torn down.
+          }
+          if (!shell.isDisposed()) {
+            shell.dispose();
+          }
+        };
+
+    if (Display.getCurrent() == display) {
+      disposeRunnable.run();
+    } else {
+      display.syncExec(disposeRunnable);
+    }
   }
 
   private class ProgressMonitor implements IProgressMonitor {
 
+    /** IProgressMonitor.worked() reports deltas; we accumulate for the SWT 
progress bar. */
+    private int workedAccumulated;
+
+    private final AtomicBoolean workedFlushScheduled = new 
AtomicBoolean(false);
+
+    private final Runnable workedFlushRunnable =
+        () -> {
+          workedFlushScheduled.set(false);
+          synchronized (shell) {
+            if (shell.isDisposed() || wlTask.isDisposed()) {
+              return;
+            }
+            try {
+              int max = wProgressBar.getMaximum();
+              int selection = Math.min(workedAccumulated, max > 0 ? max : 
workedAccumulated);
+              wProgressBar.setSelection(selection);
+            } catch (Exception e) {
+              // Ignore race condition
+            }
+          }
+        };
+
     @Override
     public void beginTask(String message, int nrWorks) {
+      workedAccumulated = 0;
       display.asyncExec(
           () -> {
             synchronized (shell) {
@@ -220,7 +275,8 @@ public class ProgressMonitorDialog {
               try {
                 wlTask.setText(Const.NVL(message, ""));
                 wProgressBar.setMaximum(nrWorks);
-              } catch (Throwable e) {
+                wProgressBar.setSelection(0);
+              } catch (Exception e) {
                 // Ignore race condition
               }
             }
@@ -237,7 +293,7 @@ public class ProgressMonitorDialog {
               }
               try {
                 wlSubTask.setText(Const.NVL(message, ""));
-              } catch (Throwable e) {
+              } catch (Exception e) {
                 // Ignore race condition
               }
             }
@@ -251,23 +307,19 @@ public class ProgressMonitorDialog {
 
     @Override
     public void worked(int nrWorks) {
-      display.asyncExec(
-          () -> {
-            synchronized (shell) {
-              if (shell.isDisposed() || wlTask.isDisposed()) {
-                return;
-              }
-              try {
-                wProgressBar.setSelection(nrWorks);
-              } catch (Throwable e) {
-                // Ignore race condition
-              }
-            }
-          });
+      if (nrWorks <= 0) {
+        return;
+      }
+      workedAccumulated += nrWorks;
+      if (workedFlushScheduled.compareAndSet(false, true)) {
+        display.asyncExec(workedFlushRunnable);
+      }
     }
 
     @Override
     public void done() {
+      workedAccumulated = 0;
+      workedFlushScheduled.set(false);
       dispose();
     }
 
@@ -281,7 +333,7 @@ public class ProgressMonitorDialog {
               }
               try {
                 wlTask.setText(Const.NVL(taskName, ""));
-              } catch (Throwable e) {
+              } catch (Exception e) {
                 // Ignore race condition
               }
             }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditorDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditorDialog.java
index ecf7cdf5ca..9ff48c42ec 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditorDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/metadata/MetadataEditorDialog.java
@@ -79,6 +79,7 @@ public class MetadataEditorDialog extends Dialog implements 
IMetadataDialog {
     buttons.add(wCancel);
     BaseTransformDialog.positionBottomButtons(
         shell, buttons.toArray(new Button[0]), PropsUi.getMargin(), null);
+    shell.setDefaultButton(wOk);
 
     Button wHelp = editor.createHelpButton(shell);
     FormData fdHelp = new FormData();
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/core/database/dialog/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/core/database/dialog/messages/messages_en_US.properties
index efdc5a19c9..f30c00d7bc 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/core/database/dialog/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/core/database/dialog/messages/messages_en_US.properties
@@ -32,6 +32,13 @@ DatabaseExplorerDialog.NoRows.Message=The table contains no 
rows
 DatabaseExplorerDialog.NoRows.Title=No rows
 DatabaseExplorerDialog.PreviewTable.Message=Number of lines to preview (0 
means all lines)
 DatabaseExplorerDialog.PreviewTable.Title=Preview limit
+PreviewTableSettingsDialog.Error.InvalidNumber=Enter valid whole numbers for 
rows and timeout.
+PreviewTableSettingsDialog.Error.Title=Invalid input
+PreviewTableSettingsDialog.Rows.Hint=0 means no row limit on the query (fetch 
all rows).
+PreviewTableSettingsDialog.Rows.Label=Number of rows to preview
+PreviewTableSettingsDialog.Timeout.Hint=0 means no JDBC statement timeout 
(driver default). Otherwise the server should abort the query after this many 
seconds. Optional default for this field: variable HOP_QUERY_PREVIEW_TIMEOUT 
(see Variables documentation).
+PreviewTableSettingsDialog.Timeout.Label=Query timeout (seconds)
+PreviewTableSettingsDialog.Title=Table preview
 DatabaseExplorerDialog.Schemas.Label=Schemas
 DatabaseExplorerDialog.Synonyms.Label=Synonyms
 DatabaseExplorerDialog.Tables.Label=Tables
@@ -66,6 +73,7 @@ SQLEditor.ErrorExecSQL.Message=An error occured executing the 
following sql\:\n\
 SQLEditor.ErrorExecSQL.Title=Error executing SQL
 SQLEditor.LineNr.Label=Line number\: {0}
 SQLEditor.Log.SQLExecError=Error while executing\: {0}\n{1}
+SQLEditor.Monitor.Execute=Executing SQL statements...
 SQLEditor.Log.SQLExecuted=SQL executed\: {0}\n
 SQLEditor.Log.StatsExecuted={0} SQL statements executed
 SQLEditor.NoRows.Message=No results found for SQL statement\:\n\n{0}

Reply via email to