This is an automated email from the ASF dual-hosted git repository.
bamaer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 7783ee83a0 Issue #8277 : Harden the Database perspective, also fixes
#8278 (#8279)
7783ee83a0 is described below
commit 7783ee83a04e8a13cecd44fadf1a96fbfb94c498
Author: Matt Casters <[email protected]>
AuthorDate: Tue Sep 8 11:58:32 2026 +0200
Issue #8277 : Harden the Database perspective, also fixes #8278 (#8279)
Keep SQL tab memory, save/close, failures, preview LIMIT, result grids,
and query classification consistent across the perspective, floating
window, and dock.
---
.../org/apache/hop/core/database/Database.java | 4 +
.../hop/core/database/SqlQueryClassifier.java | 3 -
.../org/apache/hop/core/database/DatabaseTest.java | 14 ++
.../hop/core/database/SqlQueryClassifierTest.java | 10 ++
.../hop/databases/generic/GenericDatabaseMeta.java | 8 ++
.../databases/generic/GenericDatabaseMetaTest.java | 19 +++
.../hop/ui/core/dialog/RowPreviewSupport.java | 147 +++++++++++++++++++++
.../apache/hop/ui/core/dialog/ShowRowsDialog.java | 106 +--------------
.../main/java/org/apache/hop/ui/hopgui/HopGui.java | 9 ++
.../ui/hopgui/delegates/FileRefreshHandlerMap.java | 96 ++++++++++++++
.../ui/hopgui/delegates/HopGuiFileDelegate.java | 6 +-
.../delegates/HopGuiFileRefreshDelegate.java | 52 +++++---
.../perspective/database/DatabaseOperation.java | 7 +
.../database/DatabaseOperationsPanel.java | 77 ++++++++++-
.../perspective/database/DatabaseResultsPanel.java | 10 +-
.../perspective/database/DatabaseSqlEditorTab.java | 56 +++++++-
.../perspective/database/DatabaseSqlTabMemory.java | 135 ++++++++++++++++++-
.../perspective/database/DatabaseWorkbench.java | 62 +++++++--
.../database/DatabaseWorkbenchDialog.java | 10 +-
.../database/DatabaseWorkbenchViews.java | 9 ++
.../database/HopGuiDatabaseWorkbenchHost.java | 10 ++
.../perspective/explorer/ExplorerPerspective.java | 6 +-
.../hop/ui/hopgui/terminal/HopGuiBottomDock.java | 9 ++
.../database/messages/messages_en_US.properties | 2 +
.../hop/ui/core/dialog/RowPreviewSupportTest.java | 68 ++++++++++
.../delegates/FileRefreshHandlerMapTest.java | 56 ++++++++
.../database/DatabaseOperationsPanelTest.java | 31 ++++-
.../database/DatabaseWorkbenchPreviewSqlTest.java | 14 ++
.../database/DatabaseWorkbenchViewsTest.java | 5 +
29 files changed, 888 insertions(+), 153 deletions(-)
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 1b25d89990..a66a658906 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
@@ -624,10 +624,12 @@ public class Database implements IVariables,
ILoggingObject, AutoCloseable {
openQueryStatement = null;
openQueryRowMeta = null;
if (connection == null) {
+ dbmd = null;
return; // Nothing to do...
}
try {
if (connection.isClosed()) {
+ dbmd = null;
return; // Nothing to do...
}
} catch (SQLException ex) {
@@ -723,6 +725,7 @@ public class Database implements IVariables,
ILoggingObject, AutoCloseable {
log.logError(Const.getStackTracker(hde));
}
statementQueryTimeoutSeconds = 0;
+ dbmd = null;
}
}
@@ -738,6 +741,7 @@ public class Database implements IVariables,
ILoggingObject, AutoCloseable {
connection.close();
connection = null;
}
+ dbmd = null;
if (log.isDetailed()) {
log.logDetailed("Connection to database closed!");
diff --git
a/core/src/main/java/org/apache/hop/core/database/SqlQueryClassifier.java
b/core/src/main/java/org/apache/hop/core/database/SqlQueryClassifier.java
index 3349daf093..30039f6e47 100644
--- a/core/src/main/java/org/apache/hop/core/database/SqlQueryClassifier.java
+++ b/core/src/main/java/org/apache/hop/core/database/SqlQueryClassifier.java
@@ -220,9 +220,6 @@ public final class SqlQueryClassifier {
if ("INTO".equals(keyword)) {
return true;
}
- if ("FROM".equals(keyword) || "WHERE".equals(keyword)) {
- return false;
- }
if (keyword != null) {
i = skipKeyword(sql, i);
continue;
diff --git a/core/src/test/java/org/apache/hop/core/database/DatabaseTest.java
b/core/src/test/java/org/apache/hop/core/database/DatabaseTest.java
index 2fcb33c1fe..5f2dd67a60 100644
--- a/core/src/test/java/org/apache/hop/core/database/DatabaseTest.java
+++ b/core/src/test/java/org/apache/hop/core/database/DatabaseTest.java
@@ -522,6 +522,20 @@ class DatabaseTest {
verify(conn, never()).close();
}
+ @Test
+ void disconnectClearsCachedDatabaseMetaData() throws Exception {
+ Database db = new Database(log, variables, meta);
+ Connection connection = mockConnection(dbMetaData);
+ db.setConnection(connection);
+ assertNotNull(db.getDatabaseMetaData());
+
+ db.disconnect();
+
+ Field field = Database.class.getDeclaredField("dbmd");
+ field.setAccessible(true);
+ assertNull(field.get(db));
+ }
+
@Test
void testGetTablenames() throws SQLException, HopDatabaseException {
when(rs.next()).thenReturn(true, false);
diff --git
a/core/src/test/java/org/apache/hop/core/database/SqlQueryClassifierTest.java
b/core/src/test/java/org/apache/hop/core/database/SqlQueryClassifierTest.java
index d976017e1b..d6fe647bcb 100644
---
a/core/src/test/java/org/apache/hop/core/database/SqlQueryClassifierTest.java
+++
b/core/src/test/java/org/apache/hop/core/database/SqlQueryClassifierTest.java
@@ -46,6 +46,16 @@ class SqlQueryClassifierTest {
@Test
void selectIntoIsNotAQuery() {
assertFalse(SqlQueryClassifier.isQuery("SELECT * INTO dest FROM src"));
+ assertFalse(SqlQueryClassifier.isQuery("SELECT id FROM customers INTO
@last_id"));
+ assertFalse(
+ SqlQueryClassifier.isQuery("SELECT id, name FROM customers INTO
OUTFILE '/tmp/c.csv'"));
+ assertFalse(SqlQueryClassifier.isQuery("WITH s AS (SELECT 1) SELECT * FROM
s INTO @x"));
+ }
+
+ @Test
+ void intoInsideAStringOrSubqueryDoesNotHideASelect() {
+ assertTrue(SqlQueryClassifier.isQuery("SELECT * FROM t WHERE note =
'insert into x'"));
+ assertTrue(SqlQueryClassifier.isQuery("SELECT * FROM (SELECT * INTO dest
FROM src) s"));
}
@Test
diff --git
a/plugins/databases/generic/src/main/java/org/apache/hop/databases/generic/GenericDatabaseMeta.java
b/plugins/databases/generic/src/main/java/org/apache/hop/databases/generic/GenericDatabaseMeta.java
index a78a3bfb14..bf3b835992 100644
---
a/plugins/databases/generic/src/main/java/org/apache/hop/databases/generic/GenericDatabaseMeta.java
+++
b/plugins/databases/generic/src/main/java/org/apache/hop/databases/generic/GenericDatabaseMeta.java
@@ -478,6 +478,14 @@ public class GenericDatabaseMeta extends BaseDatabaseMeta
implements IDatabase {
return super.getLimitClause(nrRows);
}
+ @Override
+ public String getLimitClausePrefix(int nrRows) {
+ if (databaseDialect != null) {
+ return databaseDialect.getLimitClausePrefix(nrRows);
+ }
+ return super.getLimitClausePrefix(nrRows);
+ }
+
@Override
public String getSelectCountStatement(String tableName) {
if (databaseDialect != null) {
diff --git
a/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDatabaseMetaTest.java
b/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDatabaseMetaTest.java
index 14f1ba316e..7b5d872541 100644
---
a/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDatabaseMetaTest.java
+++
b/plugins/databases/generic/src/test/java/org/apache/hop/databases/generic/GenericDatabaseMetaTest.java
@@ -230,6 +230,25 @@ class GenericDatabaseMetaTest {
nativeMeta.getSqlInsertAutoIncUnknownDimensionRow("FOO", "FOOKEY",
"FOOVERSION"));
}
+ @Test
+ void limitClausePrefixDelegatesToDialect() {
+ String dialect = "mssql";
+ IDatabase dialectMeta = Mockito.mock(IDatabase.class);
+ Mockito.when(dialectMeta.getPluginName()).thenReturn(dialect);
+ Mockito.when(dialectMeta.getLimitClausePrefix(25)).thenReturn(" TOP 25");
+ IDatabase[] dbInterfaces = new IDatabase[] {dialectMeta};
+ try (MockedStatic<DatabaseMeta> dbMetaStatic =
Mockito.mockStatic(DatabaseMeta.class)) {
+
dbMetaStatic.when(DatabaseMeta::getDatabaseInterfaces).thenReturn(dbInterfaces);
+ nativeMeta.setDatabaseDialect(dialect);
+ assertEquals(" TOP 25", nativeMeta.getLimitClausePrefix(25));
+ }
+ }
+
+ @Test
+ void limitClausePrefixWithoutDialectIsEmpty() {
+ assertEquals("", nativeMeta.getLimitClausePrefix(10));
+ }
+
@Test
void testSettingDialect() {
String dialect = "testDialect";
diff --git
a/ui/src/main/java/org/apache/hop/ui/core/dialog/RowPreviewSupport.java
b/ui/src/main/java/org/apache/hop/ui/core/dialog/RowPreviewSupport.java
new file mode 100644
index 0000000000..19f9e64a9e
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/RowPreviewSupport.java
@@ -0,0 +1,147 @@
+/*
+ * 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.dialog;
+
+import java.util.Objects;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.config.HopConfig;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.widget.ColumnInfo;
+import org.apache.hop.ui.core.widget.TableView;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.TableItem;
+
+/** Shared row-preview formatting used by {@link ShowRowsDialog} and the
Database results panel. */
+public final class RowPreviewSupport {
+
+ private static final Class<?> PKG = ShowRowsDialog.class;
+
+ private RowPreviewSupport() {}
+
+ public static boolean avoidBinaryHexPreview() {
+ return Const.toBoolean(
+
HopConfig.readStringVariable(Const.HOP_BINARY_FIELDS_AVOID_HEX_PREVIEW,
"false"));
+ }
+
+ public static String formatCell(IValueMeta valueMeta, Object value) throws
HopValueException {
+ return formatCell(valueMeta, value, avoidBinaryHexPreview());
+ }
+
+ static String formatCell(IValueMeta valueMeta, Object value, boolean
avoidHex)
+ throws HopValueException {
+ if (valueMeta == null) {
+ return null;
+ }
+ if (valueMeta.isBinary()) {
+ byte[] bytes = valueMeta.getBinary(value);
+ if (bytes == null) {
+ return null;
+ }
+ String display = avoidHex ? valueMeta.getString(bytes) :
Hex.encodeHexString(bytes);
+ if (display != null && display.length() >
PreviewRowsDialog.MAX_BINARY_STRING_PREVIEW_SIZE) {
+ return display.substring(0,
PreviewRowsDialog.MAX_BINARY_STRING_PREVIEW_SIZE);
+ }
+ return display;
+ }
+ return valueMeta.getString(value);
+ }
+
+ public static String formatColumnMetaTooltip(IValueMeta valueMeta) {
+ if (valueMeta == null) {
+ return null;
+ }
+ StringBuilder tip = new StringBuilder();
+ tip.append(
+ BaseMessages.getString(
+ PKG, "ShowRowsDialog.CellTooltip.Name",
Const.NVL(valueMeta.getName(), "")));
+ tip.append(Const.CR);
+ tip.append(
+ BaseMessages.getString(
+ PKG, "ShowRowsDialog.CellTooltip.Type",
Const.NVL(valueMeta.getTypeDesc(), "")));
+ if (valueMeta.getLength() > 0) {
+ tip.append(Const.CR);
+ tip.append(
+ BaseMessages.getString(
+ PKG, "ShowRowsDialog.CellTooltip.Length",
Integer.toString(valueMeta.getLength())));
+ }
+ if (valueMeta.getPrecision() > 0) {
+ tip.append(Const.CR);
+ tip.append(
+ BaseMessages.getString(
+ PKG,
+ "ShowRowsDialog.CellTooltip.Precision",
+ Integer.toString(valueMeta.getPrecision())));
+ }
+ if (!Utils.isEmpty(valueMeta.getOrigin())) {
+ tip.append(Const.CR);
+ tip.append(
+ BaseMessages.getString(PKG, "ShowRowsDialog.CellTooltip.Origin",
valueMeta.getOrigin()));
+ }
+ return tip.toString();
+ }
+
+ public static void applyColumnMeta(ColumnInfo column, IValueMeta valueMeta) {
+ if (column == null || valueMeta == null) {
+ return;
+ }
+ column.setToolTip(formatColumnMetaTooltip(valueMeta));
+ column.setValueMeta(valueMeta);
+ column.setImage(GuiResource.getInstance().getImage(valueMeta));
+ column.setReadOnly(true);
+ }
+
+ public static void installCellTooltips(TableView tableView, IRowMeta
rowMeta) {
+ if (tableView == null || tableView.isDisposed() || rowMeta == null) {
+ return;
+ }
+ tableView.table.addListener(
+ SWT.MouseMove,
+ event -> {
+ int dataColumn = dataColumnAt(tableView, rowMeta, new Point(event.x,
event.y));
+ String tip =
+ dataColumn < 0 ? null :
formatColumnMetaTooltip(rowMeta.getValueMeta(dataColumn));
+ if (!Objects.equals(tip, tableView.table.getToolTipText())) {
+ tableView.table.setToolTipText(tip);
+ }
+ });
+ }
+
+ static int dataColumnAt(TableView tableView, IRowMeta rowMeta, Point point) {
+ if (tableView == null || tableView.isDisposed() || rowMeta == null ||
point == null) {
+ return -1;
+ }
+ TableItem item = tableView.table.getItem(point);
+ if (item == null) {
+ return -1;
+ }
+ for (int i = 1; i < tableView.table.getColumnCount(); i++) {
+ if (item.getBounds(i).contains(point)) {
+ int dataColumn = i - 1;
+ return dataColumn < rowMeta.size() ? dataColumn : -1;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
index b0a80ff4ac..c3661f0e4d 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/ShowRowsDialog.java
@@ -18,8 +18,6 @@
package org.apache.hop.ui.core.dialog;
import java.util.List;
-import java.util.Objects;
-import org.apache.commons.codec.binary.Hex;
import org.apache.hop.core.Const;
import org.apache.hop.core.config.HopConfig;
import org.apache.hop.core.exception.HopValueException;
@@ -35,7 +33,6 @@ import org.apache.hop.ui.core.gui.WindowProperty;
import org.apache.hop.ui.core.widget.ColumnInfo;
import org.apache.hop.ui.core.widget.TableView;
import org.eclipse.swt.SWT;
-import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
@@ -58,9 +55,6 @@ public final class ShowRowsDialog {
private static final Class<?> PKG = ShowRowsDialog.class;
- private static final int MAX_BINARY_STRING_PREVIEW_SIZE =
- PreviewRowsDialog.MAX_BINARY_STRING_PREVIEW_SIZE;
-
private static final boolean AVOID_BINARY_IN_HEX =
Const.toBoolean(
HopConfig.readStringVariable(Const.HOP_BINARY_FIELDS_AVOID_HEX_PREVIEW,
"false"));
@@ -131,7 +125,7 @@ public final class ShowRowsDialog {
tableView = buildTableView(margin, messageLabel);
populateRows();
- setupCellTooltip();
+ RowPreviewSupport.installCellTooltips(tableView, rowMeta);
BaseDialog.defaultShellHandling(shell, c -> close(), c -> close());
}
@@ -142,12 +136,7 @@ public final class ShowRowsDialog {
IValueMeta valueMeta = rowMeta.getValueMeta(i);
columns[i] =
new ColumnInfo(valueMeta.getName(), ColumnInfo.COLUMN_TYPE_TEXT,
valueMeta.isNumeric());
- columns[i].setToolTip(formatColumnMetaTooltip(valueMeta));
- columns[i].setValueMeta(valueMeta);
- columns[i].setImage(GuiResource.getInstance().getImage(valueMeta));
- // Read-only: the grid gives such a column a view-only inline editor
with an expand icon, so
- // the full value stays selectable in place and can be opened in the
multi-line viewer.
- columns[i].setReadOnly(true);
+ RowPreviewSupport.applyColumnMeta(columns[i], valueMeta);
}
TableView view =
@@ -208,20 +197,7 @@ public final class ShowRowsDialog {
IValueMeta valueMeta = rowMeta.getValueMeta(column);
String displayValue;
try {
- if (valueMeta.isBinary()) {
- byte[] bytes = valueMeta.getBinary(row[column]);
- if (bytes == null) {
- displayValue = null;
- } else {
- displayValue =
- AVOID_BINARY_IN_HEX ? valueMeta.getString(bytes) :
Hex.encodeHexString(bytes);
- if (displayValue != null && displayValue.length() >
MAX_BINARY_STRING_PREVIEW_SIZE) {
- displayValue = displayValue.substring(0,
MAX_BINARY_STRING_PREVIEW_SIZE);
- }
- }
- } else {
- displayValue = valueMeta.getString(row[column]);
- }
+ displayValue = RowPreviewSupport.formatCell(valueMeta, row[column],
AVOID_BINARY_IN_HEX);
} catch (HopValueException | ArrayIndexOutOfBoundsException e) {
new LogChannel(PKG).logError("Unable to format cell value", e);
displayValue = null;
@@ -247,81 +223,7 @@ public final class ShowRowsDialog {
shell.dispose();
}
- /**
- * Hovering a cell shows column metadata in a tooltip: name, type, length,
precision. Selecting a
- * cell is handled by the grid itself: a read-only column gets a view-only
inline editor holding
- * the full value, with an expand icon for the multi-line viewer.
- */
- private void setupCellTooltip() {
- tableView.table.addListener(
- SWT.MouseMove,
- event -> {
- int dataColumn = dataColumnAt(new Point(event.x, event.y));
- String tip =
- dataColumn < 0 ? null :
formatColumnMetaTooltip(rowMeta.getValueMeta(dataColumn));
- if (!Objects.equals(tip, tableView.table.getToolTipText())) {
- tableView.table.setToolTipText(tip);
- }
- });
- }
-
- /**
- * Build a multi-line tooltip describing a column: name, type, and optional
length / precision /
- * origin.
- */
static String formatColumnMetaTooltip(IValueMeta valueMeta) {
- if (valueMeta == null) {
- return null;
- }
- StringBuilder tip = new StringBuilder();
- tip.append(
- BaseMessages.getString(
- PKG, "ShowRowsDialog.CellTooltip.Name",
Const.NVL(valueMeta.getName(), "")));
- tip.append(Const.CR);
- tip.append(
- BaseMessages.getString(
- PKG, "ShowRowsDialog.CellTooltip.Type",
Const.NVL(valueMeta.getTypeDesc(), "")));
- if (valueMeta.getLength() > 0) {
- tip.append(Const.CR);
- tip.append(
- BaseMessages.getString(
- PKG, "ShowRowsDialog.CellTooltip.Length",
Integer.toString(valueMeta.getLength())));
- }
- if (valueMeta.getPrecision() > 0) {
- tip.append(Const.CR);
- tip.append(
- BaseMessages.getString(
- PKG,
- "ShowRowsDialog.CellTooltip.Precision",
- Integer.toString(valueMeta.getPrecision())));
- }
- if (!Utils.isEmpty(valueMeta.getOrigin())) {
- tip.append(Const.CR);
- tip.append(
- BaseMessages.getString(PKG, "ShowRowsDialog.CellTooltip.Origin",
valueMeta.getOrigin()));
- }
- return tip.toString();
- }
-
- /**
- * The 0-based data column under a table-relative point, or -1 when the
point isn't over a data
- * cell (the row-number column, empty space, or a column outside the row
metadata).
- */
- private int dataColumnAt(Point point) {
- if (tableView == null || tableView.isDisposed() || rowMeta == null) {
- return -1;
- }
- TableItem item = tableView.table.getItem(point);
- if (item == null) {
- return -1;
- }
- // Column 0 is the row-number column; data columns start at 1. Find the
one under the pointer.
- for (int i = 1; i < tableView.table.getColumnCount(); i++) {
- if (item.getBounds(i).contains(point)) {
- int dataColumn = i - 1;
- return dataColumn < rowMeta.size() ? dataColumn : -1;
- }
- }
- return -1;
+ return RowPreviewSupport.formatColumnMetaTooltip(valueMeta);
}
}
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 88bf432e52..3605a396c2 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -134,6 +134,7 @@ import
org.apache.hop.ui.hopgui.perspective.HopPerspectivePlugin;
import org.apache.hop.ui.hopgui.perspective.HopPerspectivePluginType;
import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
import
org.apache.hop.ui.hopgui.perspective.configuration.ConfigurationPerspective;
+import org.apache.hop.ui.hopgui.perspective.database.DatabaseSqlEditorTab;
import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
import org.apache.hop.ui.hopgui.perspective.metadata.MetadataPerspective;
@@ -351,6 +352,7 @@ public class HopGui
private Composite mainPerspectivesComposite;
private HopPerspectiveManager perspectiveManager;
private IHopPerspective activePerspective;
+ private IHopFileTypeHandler capabilityFileTypeHandler;
private org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock terminalPanel;
public org.apache.hop.ui.hopgui.terminal.HopGuiBottomDock getTerminalPanel()
{
@@ -2323,6 +2325,8 @@ public class HopGui
boolean running,
boolean paused) {
+ this.capabilityFileTypeHandler = handler;
+
mainMenuWidgets.enableMenuItem(
fileType, handler, ID_MAIN_MENU_FILE_SAVE,
IHopFileType.CAPABILITY_SAVE, changed);
mainMenuWidgets.enableMenuItem(
@@ -2389,6 +2393,11 @@ public class HopGui
}
public IHopFileTypeHandler getActiveFileTypeHandler() {
+ if (capabilityFileTypeHandler instanceof DatabaseSqlEditorTab tab
+ && tab.getControl() != null
+ && !tab.getControl().isDisposed()) {
+ return tab;
+ }
return getActivePerspective().getActiveFileTypeHandler();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/FileRefreshHandlerMap.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/FileRefreshHandlerMap.java
new file mode 100644
index 0000000000..fb6376d80c
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/FileRefreshHandlerMap.java
@@ -0,0 +1,96 @@
+/*
+ * 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.hopgui.delegates;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+
+/** Path → handlers. Several editors can watch the same file; the last close
unwatches. */
+final class FileRefreshHandlerMap {
+
+ private final Map<String, List<IHopFileTypeHandler>> handlers = new
HashMap<>();
+
+ /**
+ * @return {@code true} when this is the first handler for {@code key}
+ */
+ boolean add(String key, IHopFileTypeHandler handler) {
+ if (key == null || handler == null) {
+ return false;
+ }
+ List<IHopFileTypeHandler> list = handlers.computeIfAbsent(key, k -> new
ArrayList<>());
+ if (list.contains(handler)) {
+ return false;
+ }
+ list.add(handler);
+ return list.size() == 1;
+ }
+
+ /** Make {@code aliasKey} share the handler list of {@code existingKey}. */
+ void alias(String existingKey, String aliasKey) {
+ if (existingKey == null || aliasKey == null ||
existingKey.equals(aliasKey)) {
+ return;
+ }
+ List<IHopFileTypeHandler> list = handlers.get(existingKey);
+ if (list != null) {
+ handlers.put(aliasKey, list);
+ }
+ }
+
+ List<IHopFileTypeHandler> get(String key) {
+ List<IHopFileTypeHandler> list = handlers.get(key);
+ return list == null ? List.of() : List.copyOf(list);
+ }
+
+ /**
+ * @return {@code true} when no handlers remain for {@code key} (the watch
can be dropped)
+ */
+ boolean remove(String key, IHopFileTypeHandler handler) {
+ if (key == null) {
+ return true;
+ }
+ List<IHopFileTypeHandler> list = handlers.get(key);
+ if (list == null) {
+ return true;
+ }
+ list.remove(handler);
+ if (list.isEmpty()) {
+ removeKeysForList(list);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return {@code true} when the path is no longer watched
+ */
+ boolean removeAll(String key) {
+ List<IHopFileTypeHandler> list = handlers.get(key);
+ if (list == null) {
+ return true;
+ }
+ removeKeysForList(list);
+ return true;
+ }
+
+ private void removeKeysForList(List<IHopFileTypeHandler> list) {
+ handlers.entrySet().removeIf(entry -> entry.getValue() == list);
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
index 154e8236fb..0fc981b990 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileDelegate.java
@@ -59,6 +59,7 @@ import
org.apache.hop.ui.hopgui.file.pipeline.HopGuiPipelineGraph;
import org.apache.hop.ui.hopgui.file.workflow.HopGuiWorkflowGraph;
import org.apache.hop.ui.hopgui.perspective.IHopPerspective;
import org.apache.hop.ui.hopgui.perspective.TabItemHandler;
+import org.apache.hop.ui.hopgui.perspective.database.DatabaseSqlEditorTab;
import org.apache.hop.ui.hopgui.perspective.execution.ExecutionPerspective;
import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
import org.apache.hop.ui.util.EnvironmentUtils;
@@ -271,7 +272,10 @@ public class HopGuiFileDelegate {
IHopFileTypeHandler typeHandler = getActiveFileTypeHandler();
IHopFileType fileType = typeHandler.getFileType();
if (fileType.hasCapability(IHopFileType.CAPABILITY_CLOSE)) {
- boolean removed = perspective.remove(typeHandler);
+ boolean removed =
+ typeHandler instanceof DatabaseSqlEditorTab sqlTab
+ ? sqlTab.requestClose()
+ : perspective.remove(typeHandler);
if (removed) {
hopGui.auditDelegate.writeLastOpenFiles();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileRefreshDelegate.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileRefreshDelegate.java
index ee916d5ea5..4e1c22d3ea 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileRefreshDelegate.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/delegates/HopGuiFileRefreshDelegate.java
@@ -17,8 +17,6 @@
package org.apache.hop.ui.hopgui.delegates;
-import java.util.HashMap;
-import java.util.Map;
import org.apache.commons.vfs2.FileChangeEvent;
import org.apache.commons.vfs2.FileListener;
import org.apache.commons.vfs2.FileObject;
@@ -34,17 +32,14 @@ public class HopGuiFileRefreshDelegate {
private DefaultFileMonitor fileMonitor;
- // key: vfs file uri
- // value: the corresponding fileTypeHandler
- //
- private Map<String, IHopFileTypeHandler> fileHandlerMap;
+ private final FileRefreshHandlerMap fileHandlerMap;
// TODO: replace it with a config option
private static final long DELAY = 1000l;
public HopGuiFileRefreshDelegate(HopGui hopGui) {
this.hopGui = hopGui;
- this.fileHandlerMap = new HashMap<>();
+ this.fileHandlerMap = new FileRefreshHandlerMap();
this.fileMonitor =
new DefaultFileMonitor(
new FileListener() {
@@ -52,9 +47,8 @@ public class HopGuiFileRefreshDelegate {
@Override
public void fileChanged(FileChangeEvent arg0) throws Exception {
String fileName = arg0.getFileObject().getName().getURI();
- if (fileName != null) {
- IHopFileTypeHandler fileHandler =
fileHandlerMap.get(fileName);
- if (fileHandler != null &&
!hopGui.getDisplay().isDisposed()) {
+ if (fileName != null && !hopGui.getDisplay().isDisposed()) {
+ for (IHopFileTypeHandler fileHandler :
fileHandlerMap.get(fileName)) {
hopGui.getDisplay().asyncExec(fileHandler::reload);
}
}
@@ -86,26 +80,48 @@ public class HopGuiFileRefreshDelegate {
try {
FileObject file = HopVfs.getFileObject(fileName);
- fileMonitor.addFile(file);
- fileHandlerMap.put(file.getPublicURIString(), fileTypeHandler);
+ String uri = file.getPublicURIString();
+ boolean first = fileHandlerMap.add(uri, fileTypeHandler);
+ fileHandlerMap.alias(uri, fileName);
+ if (first) {
+ fileMonitor.addFile(file);
+ }
} catch (HopFileException e) {
hopGui.getLog().logError("Error registering new FileObject", e);
+ fileHandlerMap.add(fileName, fileTypeHandler);
}
- fileHandlerMap.put(fileName, fileTypeHandler);
}
public void remove(String fileName) {
- if (!hopGui.getProps().isReloadingFilesOnChange()) {
+ remove(fileName, null);
+ }
+
+ public void remove(String fileName, IHopFileTypeHandler fileTypeHandler) {
+ if (fileName == null || !hopGui.getProps().isReloadingFilesOnChange()) {
return;
}
try {
FileObject file = HopVfs.getFileObject(fileName);
- fileName = file.getPublicURIString();
- fileMonitor.removeFile(file);
+ String uri = file.getPublicURIString();
+ boolean empty =
+ fileTypeHandler == null
+ ? fileHandlerMap.removeAll(uri)
+ : fileHandlerMap.remove(uri, fileTypeHandler);
+ if (fileTypeHandler != null) {
+ fileHandlerMap.remove(fileName, fileTypeHandler);
+ } else {
+ fileHandlerMap.removeAll(fileName);
+ }
+ if (empty) {
+ fileMonitor.removeFile(file);
+ }
} catch (HopFileException e) {
hopGui.getLog().logError("Error removing FileObject from fileListener",
e);
- } finally {
- fileHandlerMap.remove(fileName);
+ if (fileTypeHandler == null) {
+ fileHandlerMap.removeAll(fileName);
+ } else {
+ fileHandlerMap.remove(fileName, fileTypeHandler);
+ }
}
}
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
index 870e4bef01..9ce401ba12 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperation.java
@@ -74,6 +74,7 @@ public class DatabaseOperation {
// Best-effort; the worker still sees isCancelled().
}
}
+ detachDatabase();
}
public boolean isCancelled() {
@@ -87,12 +88,18 @@ public class DatabaseOperation {
public void complete() {
status = cancelled ? Status.CANCELLED : Status.DONE;
endTime = System.currentTimeMillis();
+ detachDatabase();
}
public void fail(String message) {
status = cancelled ? Status.CANCELLED : Status.FAILED;
errorMessage = message;
endTime = System.currentTimeMillis();
+ detachDatabase();
+ }
+
+ void detachDatabase() {
+ database.set(null);
}
public long elapsedMillis() {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanel.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanel.java
index 024d8af2e9..5f9e2eed67 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanel.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanel.java
@@ -20,6 +20,7 @@ package org.apache.hop.ui.hopgui.perspective.database;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
+import java.util.Objects;
import java.util.function.Consumer;
import lombok.Getter;
import org.apache.hop.core.Const;
@@ -33,6 +34,7 @@ import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.hopgui.ToolbarFacade;
import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
@@ -55,8 +57,11 @@ public class DatabaseOperationsPanel extends Composite {
public static final String GUI_PLUGIN_TOOLBAR_PARENT_ID =
"DatabaseOperationsPanel-Toolbar";
public static final String TOOLBAR_ITEM_KILL =
"DatabaseOperations-Toolbar-10000-Kill";
+ public static final String TOOLBAR_ITEM_CLEAR =
"DatabaseOperations-Toolbar-10005-Clear";
public static final String TOOLBAR_ITEM_MINIMIZE =
"DatabaseOperations-Toolbar-10010-Minimize";
+ static final int MAX_FINISHED_OPERATIONS = 50;
+
public static final String GUI_PLUGIN_STATUS_TOOLBAR_PARENT_ID =
"DatabaseOperationsStatus-Toolbar";
public static final String TOOLBAR_ITEM_STATUS_KILL =
@@ -97,6 +102,19 @@ public class DatabaseOperationsPanel extends Composite {
table.setLayoutData(
new FormDataBuilder().top(toolBar,
PropsUi.getMargin()).bottom().fullWidth().result());
table.addListener(SWT.Selection, e -> updateKillEnablement());
+ table.addListener(
+ SWT.MouseMove,
+ e -> {
+ TableItem item = table.getItem(new Point(e.x, e.y));
+ String tip = "";
+ if (item != null && item.getData() instanceof DatabaseOperation
operation) {
+ tip =
+ Const.NVL(operation.getErrorMessage(),
Const.NVL(formatStatusLine(operation), ""));
+ }
+ if (!Objects.equals(tip, table.getToolTipText())) {
+ table.setToolTipText(tip);
+ }
+ });
addColumn(
BaseMessages.getString(PKG,
"DatabasePerspective.Operations.Column.Description"), 280);
@@ -159,8 +177,8 @@ public class DatabaseOperationsPanel extends Composite {
public void addOperation(DatabaseOperation operation) {
operations.add(0, operation);
- TableItem item = new TableItem(table, SWT.NONE, 0);
- fillItem(item, operation);
+ trimFinished(operations, MAX_FINISHED_OPERATIONS);
+ rebuildTable();
table.setSelection(0);
updateStatusLine();
updateKillEnablement();
@@ -179,6 +197,48 @@ public class DatabaseOperationsPanel extends Composite {
armTimer();
}
+ public void clearFinished() {
+ operations.removeIf(DatabaseOperation::isFinished);
+ rebuildTable();
+ updateStatusLine();
+ updateKillEnablement();
+ }
+
+ public void clearAll() {
+ operations.clear();
+ rebuildTable();
+ updateStatusLine();
+ updateKillEnablement();
+ }
+
+ private void rebuildTable() {
+ if (table == null || table.isDisposed()) {
+ return;
+ }
+ table.removeAll();
+ for (DatabaseOperation operation : operations) {
+ TableItem item = new TableItem(table, SWT.NONE);
+ fillItem(item, operation);
+ }
+ }
+
+ static void trimFinished(List<DatabaseOperation> operations, int
maxFinished) {
+ if (operations == null || maxFinished < 0) {
+ return;
+ }
+ int finished = 0;
+ for (int i = 0; i < operations.size(); i++) {
+ if (!operations.get(i).isFinished()) {
+ continue;
+ }
+ finished++;
+ if (finished > maxFinished) {
+ operations.remove(i);
+ i--;
+ }
+ }
+ }
+
private void refreshElapsed() {
timerArmed = false;
if (isDisposed()) {
@@ -258,6 +318,10 @@ public class DatabaseOperationsPanel extends Composite {
}
line.append(" - ").append(statusLabel(operation));
line.append(" - ").append(formatElapsed(operation.elapsedMillis()));
+ if (operation.getStatus() == DatabaseOperation.Status.FAILED
+ && !Utils.isEmpty(operation.getErrorMessage())) {
+ line.append(" - ").append(operation.getErrorMessage());
+ }
return line.toString();
}
@@ -285,6 +349,15 @@ public class DatabaseOperationsPanel extends Composite {
}
}
+ @GuiToolbarElement(
+ root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
+ id = TOOLBAR_ITEM_CLEAR,
+ toolTip = "i18n::DatabasePerspective.Operations.Clear.Tooltip",
+ image = "ui/images/clear.svg")
+ public void clearFinishedFromToolbar() {
+ clearFinished();
+ }
+
@GuiToolbarElement(
root = GUI_PLUGIN_TOOLBAR_PARENT_ID,
id = TOOLBAR_ITEM_MINIMIZE,
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseResultsPanel.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseResultsPanel.java
index e959df55df..6438d309ec 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseResultsPanel.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseResultsPanel.java
@@ -27,6 +27,7 @@ import org.apache.hop.core.variables.IVariables;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.FormDataBuilder;
import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.RowPreviewSupport;
import org.apache.hop.ui.core.dialog.ShowRowsDialog;
import org.apache.hop.ui.core.gui.GuiResource;
import org.apache.hop.ui.core.widget.ColumnInfo;
@@ -187,9 +188,7 @@ public class DatabaseResultsPanel extends Composite {
IValueMeta valueMeta = rowMeta.getValueMeta(i);
columnInfos[i] =
new ColumnInfo(valueMeta.getName(), ColumnInfo.COLUMN_TYPE_TEXT,
valueMeta.isNumeric());
- columnInfos[i].setValueMeta(valueMeta);
- columnInfos[i].setReadOnly(true);
- columnInfos[i].setImage(GuiResource.getInstance().getImage(valueMeta));
+ RowPreviewSupport.applyColumnMeta(columnInfos[i], valueMeta);
}
TableView view =
@@ -206,6 +205,9 @@ public class DatabaseResultsPanel extends Composite {
view.setShortenDisplayedValues(true);
view.setSortable(true);
view.setReadonly(true);
+ if (rowMeta != null) {
+ RowPreviewSupport.installCellTooltips(view, rowMeta);
+ }
if (result.rows != null && rowMeta != null) {
int lineNr = 0;
@@ -220,7 +222,7 @@ public class DatabaseResultsPanel extends Composite {
for (int c = 0; c < rowMeta.size(); c++) {
String display;
try {
- display = rowMeta.getValueMeta(c).getString(row[c]);
+ display = RowPreviewSupport.formatCell(rowMeta.getValueMeta(c),
row[c]);
} catch (HopValueException | ArrayIndexOutOfBoundsException e) {
display = null;
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
index 48724e1366..34168870fa 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlEditorTab.java
@@ -38,6 +38,7 @@ import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.gui.plugin.GuiPlugin;
import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementFilter;
+import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.util.EnvUtil;
import org.apache.hop.core.util.Utils;
@@ -46,6 +47,7 @@ import org.apache.hop.core.vfs.HopVfs;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.FormDataBuilder;
import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.BaseDialog;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.dialog.MessageBox;
import org.apache.hop.ui.core.gui.GuiResource;
@@ -429,13 +431,26 @@ public class DatabaseSqlEditorTab implements
IHopFileTypeHandler {
.append(Const.CR);
}
}
+ } catch (Exception e) {
+ if (!messages.isEmpty() && messages.charAt(messages.length() - 1)
!= '\n') {
+ messages.append(Const.CR);
+ }
+ messages
+ .append(BaseMessages.getString(PKG,
"DatabasePerspective.SqlTab.Failed"))
+ .append(": ")
+ .append(Const.NVL(e.getMessage(), e.toString()));
+ throw e;
+ } finally {
+ String messageText = messages.toString();
+ host.asyncExec(
+ () -> {
+ if (control.isDisposed() || resultsPanel.isDisposed()) {
+ return;
+ }
+ resultsPanel.show(queryResults, messageText);
+ showResults();
+ });
}
- String messageText = messages.toString();
- host.asyncExec(
- () -> {
- resultsPanel.show(queryResults, messageText);
- showResults();
- });
});
}
@@ -633,6 +648,19 @@ public class DatabaseSqlEditorTab implements
IHopFileTypeHandler {
}
}
+ @Override
+ public void reload() {
+ if (Utils.isEmpty(filename) || changed) {
+ return;
+ }
+ try {
+ loadFromVfs();
+ host.updateGui(this);
+ } catch (Exception e) {
+ LogChannel.UI.logError("Unable to reload SQL file '" + filename + "'",
e);
+ }
+ }
+
@Override
public void start() {}
@@ -701,7 +729,17 @@ public class DatabaseSqlEditorTab implements
IHopFileTypeHandler {
int answer = messageDialog.open();
if ((answer & SWT.YES) != 0) {
if (Utils.isEmpty(filename)) {
- host.getHopGui().fileDelegate.fileSaveAs();
+ String newFilename =
+ BaseDialog.presentFileDialog(
+ true,
+ host.getShell(),
+ FILE_TYPE.getFilterExtensions(),
+ FILE_TYPE.getFilterNames(),
+ true);
+ if (newFilename == null) {
+ return false;
+ }
+ saveAs(getVariables().resolve(newFilename));
return !changed;
}
save();
@@ -723,6 +761,10 @@ public class DatabaseSqlEditorTab implements
IHopFileTypeHandler {
workbench.remove(this);
}
+ public boolean requestClose() {
+ return workbench.remove(this);
+ }
+
@Override
public boolean hasChanged() {
return changed;
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
index ab5e050208..b509bc784d 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseSqlTabMemory.java
@@ -29,7 +29,9 @@ import org.apache.hop.history.AuditManager;
import org.apache.hop.history.AuditState;
import org.apache.hop.history.AuditStateMap;
import org.apache.hop.ui.core.gui.HopNamespace;
+import org.apache.hop.ui.hopgui.HopGui;
import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
/**
* Remembers SQL editor tabs across the Database perspective, floating window
and bottom dock, and
@@ -52,13 +54,27 @@ final class DatabaseSqlTabMemory {
static final int MAX_SQL_CHARS = 512_000;
private static final int SAVE_DEBOUNCE_MS = 400;
+ static final String SESSION_KEY = DatabaseSqlTabMemory.class.getName();
private DatabaseSqlTabMemory() {}
static void restore(DatabaseWorkbench workbench) {
- if (workbench == null || workbench.isDisposed() ||
workbench.hasSqlEditorTabs()) {
+ if (workbench == null || workbench.isDisposed()) {
+ return;
+ }
+ register(workbench);
+ if (workbench.hasSqlEditorTabs()) {
+ claim(workbench);
return;
}
+ Session existing = session(workbench);
+ if (existing != null
+ && existing.owner != null
+ && existing.owner != workbench
+ && !existing.owner.isDisposed()) {
+ handOff(existing.owner);
+ }
+ claim(workbench);
try {
String group = HopNamespace.getNamespace();
AuditList list = AuditManager.getActive().retrieveList(group,
AUDIT_TYPE);
@@ -115,7 +131,7 @@ final class DatabaseSqlTabMemory {
}
static void save(DatabaseWorkbench workbench) {
- if (workbench == null || workbench.isDisposed()) {
+ if (workbench == null || workbench.isDisposed() || !isOwner(workbench)) {
return;
}
try {
@@ -140,6 +156,121 @@ final class DatabaseSqlTabMemory {
}
}
+ static Session session(DatabaseWorkbench workbench) {
+ if (workbench == null) {
+ return null;
+ }
+ HopGui hopGui = workbench.hopGui();
+ if (hopGui == null || hopGui.getShell() == null ||
hopGui.getShell().isDisposed()) {
+ return null;
+ }
+ Shell shell = hopGui.getShell();
+ Object data = shell.getData(SESSION_KEY);
+ if (data instanceof Session existing) {
+ return existing;
+ }
+ Session created = new Session();
+ shell.setData(SESSION_KEY, created);
+ return created;
+ }
+
+ static void register(DatabaseWorkbench workbench) {
+ Session session = session(workbench);
+ if (session == null || workbench == null) {
+ return;
+ }
+ if (!session.live.contains(workbench)) {
+ session.live.add(workbench);
+ }
+ }
+
+ static void unregister(DatabaseWorkbench workbench) {
+ Session session = session(workbench);
+ if (session == null || workbench == null) {
+ return;
+ }
+ session.live.remove(workbench);
+ if (session.owner == workbench) {
+ session.owner = null;
+ }
+ }
+
+ static boolean isOwner(DatabaseWorkbench workbench) {
+ Session session = session(workbench);
+ if (session == null) {
+ return true;
+ }
+ return session.owner == workbench;
+ }
+
+ static void claim(DatabaseWorkbench workbench) {
+ Session session = session(workbench);
+ if (session != null) {
+ session.owner = workbench;
+ }
+ }
+
+ static void release(DatabaseWorkbench workbench) {
+ Session session = session(workbench);
+ if (session != null && session.owner == workbench) {
+ session.owner = null;
+ }
+ }
+
+ static void cancelScheduledSave(DatabaseWorkbench workbench) {
+ if (workbench == null || workbench.isDisposed()) {
+ return;
+ }
+ Display display = workbench.getDisplay();
+ if (display != null && !display.isDisposed()) {
+ display.timerExec(-1, workbench.persistSqlTabsRunnable);
+ }
+ }
+
+ /**
+ * Persist this workbench's tabs if it owns them, close the SQL editors
without writing again, and
+ * drop ownership so another host can restore the snapshot.
+ */
+ static void handOff(DatabaseWorkbench workbench) {
+ if (workbench == null || workbench.isDisposed()) {
+ return;
+ }
+ cancelScheduledSave(workbench);
+ saveNow(workbench);
+ workbench.closeSqlEditorTabs();
+ release(workbench);
+ }
+
+ static void ensureOwner(DatabaseWorkbench workbench) {
+ if (workbench == null || workbench.isDisposed() || isOwner(workbench)) {
+ return;
+ }
+ Session session = session(workbench);
+ DatabaseWorkbench previous = session == null ? null : session.owner;
+ if (previous != null && previous != workbench && !previous.isDisposed()) {
+ handOff(previous);
+ }
+ claim(workbench);
+ }
+
+ static void restoreIntoRemaining(DatabaseWorkbench leaving) {
+ Session session = session(leaving);
+ if (session == null) {
+ return;
+ }
+ for (DatabaseWorkbench live : new ArrayList<>(session.live)) {
+ if (live != leaving && live != null && !live.isDisposed()) {
+ restore(live);
+ return;
+ }
+ }
+ }
+
+ static final class Session {
+ final List<DatabaseWorkbench> live = new ArrayList<>();
+ DatabaseWorkbench owner;
+ }
+
static List<Snapshot> snapshotsFromAudit(AuditList list, AuditStateMap
stateMap) {
List<Snapshot> snapshots = new ArrayList<>();
if (list == null || Utils.isEmpty(list.getNames()) || stateMap == null) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
index 8bf75598f6..10b4170fac 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbench.java
@@ -32,6 +32,7 @@ import org.apache.hop.core.database.Schema;
import org.apache.hop.core.gui.plugin.GuiPlugin;
import org.apache.hop.core.gui.plugin.menu.GuiMenuElement;
import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.logging.LogChannel;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.search.SearchMatcher;
import org.apache.hop.core.util.Utils;
@@ -49,6 +50,7 @@ import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
import org.apache.hop.ui.core.gui.IToolbarContainer;
import org.apache.hop.ui.core.widget.TreeMemory;
import org.apache.hop.ui.hopgui.BackgroundThreadFacade;
+import org.apache.hop.ui.hopgui.HopGui;
import org.apache.hop.ui.hopgui.ToolbarFacade;
import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
@@ -242,23 +244,37 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
host.asyncExec(
() -> {
closeSqlEditorTabs();
+ operationsPanel.cancelAll();
+ operationsPanel.clearAll();
reloadConnections();
- DatabaseSqlTabMemory.restore(this);
+ if (DatabaseSqlTabMemory.isOwner(this)) {
+ DatabaseSqlTabMemory.restore(this);
+ }
}),
HopGuiEvents.ProjectActivated.name());
addDisposeListener(
e -> {
+ boolean wasOwner = DatabaseSqlTabMemory.isOwner(this);
DatabaseSqlTabMemory.saveNow(this);
operationsPanel.cancelAll();
+ DatabaseSqlTabMemory.unregister(this);
+ if (wasOwner) {
+ DatabaseSqlTabMemory.restoreIntoRemaining(this);
+ }
host.getHopGui().getEventsHandler().removeEventListeners(eventListenerId);
host.getHopGui().getEventsHandler().removeEventListeners(eventListenerId +
"-project");
});
+ DatabaseSqlTabMemory.register(this);
reloadConnections();
DatabaseSqlTabMemory.restore(this);
}
+ HopGui hopGui() {
+ return host.getHopGui();
+ }
+
public DatabaseSqlFileType getSqlFileType() {
return sqlFileType;
}
@@ -967,14 +983,16 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
}
/**
- * {@code SELECT * FROM schema.table} plus the dialect's limit clause ({@link
- * DatabaseMeta#getLimitClause(int)}).
+ * {@code SELECT * FROM schema.table} plus the dialect's row limit. Some
databases put the clause
+ * after {@code SELECT} ({@link DatabaseMeta#getLimitClausePrefix(int)});
others append it ({@link
+ * DatabaseMeta#getLimitClause(int)}). Matches {@code Database.getFirstRows}.
*/
static String previewSelectSql(
DatabaseMeta meta, IVariables variables, String schemaName, String
tableName, int rowLimit) {
String qualified = meta.getQuotedSchemaTableCombination(variables,
schemaName, tableName);
- String limit = Const.NVL(meta.getLimitClause(rowLimit), "");
- return "SELECT * FROM " + qualified + limit;
+ String prefix = rowLimit > 0 ?
Const.NVL(meta.getLimitClausePrefix(rowLimit), "") : "";
+ String limit = rowLimit > 0 ? Const.NVL(meta.getLimitClause(rowLimit), "")
: "";
+ return "SELECT" + prefix + " * FROM " + qualified + limit;
}
@GuiToolbarElement(
@@ -1005,7 +1023,11 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
image = "ui/images/detach-panel.svg",
separator = true)
public void openFloatingWindow() {
- DatabaseSqlTabMemory.saveNow(this);
+ if (DatabaseWorkbenchViews.isDialogOpen(host.getHopGui())) {
+ DatabaseWorkbenchViews.openDialog(host.getHopGui());
+ return;
+ }
+ DatabaseSqlTabMemory.handOff(this);
DatabaseWorkbenchViews.openDialog(host.getHopGui());
}
@@ -1015,12 +1037,19 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
toolTip = "i18n::DatabasePerspective.Toolbar.Dock.Tooltip",
image = "ui/images/dock-panel.svg")
public void openInBottomDock() {
- DatabaseSqlTabMemory.saveNow(this);
+ if (DatabaseWorkbenchViews.isDockOpen(host.getHopGui())) {
+ DatabaseWorkbenchViews.openDock(host.getHopGui());
+ return;
+ }
+ DatabaseSqlTabMemory.handOff(this);
DatabaseWorkbenchViews.openDock(host.getHopGui());
}
public DatabaseSqlEditorTab openSqlTab(
DatabaseMeta meta, String sql, String filename, String buffer, boolean
dirty) {
+ if (!restoringSqlTabs) {
+ DatabaseSqlTabMemory.ensureOwner(this);
+ }
if (!Utils.isEmpty(filename)) {
for (TabItemHandler item : items) {
if (item.getTypeHandler() instanceof DatabaseSqlEditorTab tab
@@ -1049,7 +1078,8 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
}
}
} else {
- tab.setInitialText(Const.NVL(sql, ""));
+ String text = buffer != null ? buffer : Const.NVL(sql, "");
+ tab.applyBuffer(text, dirty);
}
tab.setTabItem(tabItem);
tabItem.setControl(tab.getControl());
@@ -1263,6 +1293,7 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
work.run(operation);
operation.complete();
} catch (Exception e) {
+ LogChannel.UI.logError(description, e);
operation.fail(Const.NVL(e.getMessage(), e.toString()));
}
host.asyncExec(operationsPanel::refresh);
@@ -1322,15 +1353,28 @@ public class DatabaseWorkbench extends Composite
implements TabClosable {
items.remove(item);
IHopFileTypeHandler handler = item.getTypeHandler();
if (handler != null && handler.getFilename() != null) {
- host.getHopGui().fileRefreshDelegate.remove(handler.getFilename());
+ host.getHopGui().fileRefreshDelegate.remove(handler.getFilename(),
handler);
}
if (item.getTabItem() != null && !item.getTabItem().isDisposed()) {
+ Control content = item.getTabItem().getControl();
+ if (content != null && !content.isDisposed()) {
+ content.dispose();
+ }
item.getTabItem().dispose();
}
host.updateGui(getActiveFileTypeHandler());
schedulePersistSqlTabs();
}
+ boolean canCloseSqlTabs() {
+ for (TabItemHandler item : new ArrayList<>(items)) {
+ if (item.getTypeHandler() instanceof DatabaseSqlEditorTab tab &&
!tab.isCloseable()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
@Override
public void closeTab(CTabFolderEvent event, CTabItem tabItem) {
if (tabItem == null || tabItem.isDisposed()) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchDialog.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchDialog.java
index b54454fa52..c375a5001b 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchDialog.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchDialog.java
@@ -122,7 +122,7 @@ public class DatabaseWorkbenchDialog {
shell.setLayout(layout);
HopGuiDatabaseWorkbenchHost host =
- new HopGuiDatabaseWorkbenchHost(hopGui, this::isOpen, this::activate);
+ new HopGuiDatabaseWorkbenchHost(hopGui, this::isOpen, this::activate,
shell);
workbench = new DatabaseWorkbench(shell, host);
workbench.setLayoutData(new FormDataBuilder().fullSize().result());
@@ -131,6 +131,14 @@ public class DatabaseWorkbenchDialog {
hopGui.replaceKeyboardShortcutListeners(workbench, keyHandler);
hopGui.replaceKeyboardShortcutListeners(shell, keyHandler);
+ shell.addListener(
+ SWT.Close,
+ event -> {
+ if (workbench != null && !workbench.isDisposed() &&
!workbench.canCloseSqlTabs()) {
+ event.doit = false;
+ }
+ });
+
shell.addDisposeListener(
e -> {
props.setScreen(new WindowProperty(shell));
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViews.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViews.java
index f158fcf70c..7c19ae0e14 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViews.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViews.java
@@ -66,6 +66,15 @@ public class DatabaseWorkbenchViews {
DatabaseWorkbenchDialog.open(hopGui);
}
+ /** True when the floating Database window is already open. */
+ public static boolean isDialogOpen(HopGui hopGui) {
+ if (hopGui == null || hopGui.getShell() == null ||
hopGui.getShell().isDisposed()) {
+ return false;
+ }
+ Object existing =
hopGui.getShell().getData(DatabaseWorkbenchDialog.SHELL_DATA_KEY);
+ return existing instanceof DatabaseWorkbenchDialog dialog &&
dialog.isOpen();
+ }
+
/** Open or focus the Database tab in the bottom dock. */
public static void openDock(HopGui hopGui) {
if (hopGui == null) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
index 1536ab21f4..a56492d9b1 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/HopGuiDatabaseWorkbenchHost.java
@@ -32,11 +32,18 @@ public class HopGuiDatabaseWorkbenchHost implements
IDatabaseWorkbenchHost {
private final HopGui hopGui;
private final BooleanSupplier alive;
private final Runnable onActivate;
+ private final Shell dialogShell;
public HopGuiDatabaseWorkbenchHost(HopGui hopGui, BooleanSupplier alive,
Runnable onActivate) {
+ this(hopGui, alive, onActivate, null);
+ }
+
+ public HopGuiDatabaseWorkbenchHost(
+ HopGui hopGui, BooleanSupplier alive, Runnable onActivate, Shell
dialogShell) {
this.hopGui = hopGui;
this.alive = alive;
this.onActivate = onActivate;
+ this.dialogShell = dialogShell;
}
@Override
@@ -46,6 +53,9 @@ public class HopGuiDatabaseWorkbenchHost implements
IDatabaseWorkbenchHost {
@Override
public Shell getShell() {
+ if (dialogShell != null && !dialogShell.isDisposed()) {
+ return dialogShell;
+ }
return hopGui.getShell();
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
index 5c271227bf..985790723e 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/ExplorerPerspective.java
@@ -2004,7 +2004,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
@SuppressWarnings("javabugs:S2259") // callers always pass an open file type
handler
protected void changeFilename(IHopFileTypeHandler fileTypeHandler, String
newFilename) {
String oldFilename = fileTypeHandler.getFilename();
- hopGui.fileRefreshDelegate.remove(oldFilename);
+ hopGui.fileRefreshDelegate.remove(oldFilename, fileTypeHandler);
fileTypeHandler.setFilename(newFilename);
hopGui.fileRefreshDelegate.register(newFilename, fileTypeHandler);
}
@@ -2408,7 +2408,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
items.remove(toRemove);
IHopFileTypeHandler fileTypeHandler = toRemove.getTypeHandler();
if (fileTypeHandler != null && fileTypeHandler.getFilename() != null) {
- hopGui.fileRefreshDelegate.remove(fileTypeHandler.getFilename());
+ hopGui.fileRefreshDelegate.remove(fileTypeHandler.getFilename(),
fileTypeHandler);
}
}
tabItem.dispose();
@@ -2432,7 +2432,7 @@ public class ExplorerPerspective implements
IHopPerspective, TabClosable, IFileD
}
IHopFileTypeHandler fileTypeHandler = item.getTypeHandler();
if (fileTypeHandler != null && fileTypeHandler.getFilename() != null) {
- hopGui.fileRefreshDelegate.remove(fileTypeHandler.getFilename());
+ hopGui.fileRefreshDelegate.remove(fileTypeHandler.getFilename(),
fileTypeHandler);
}
if (!hopGui.fileDelegate.isClosing()) {
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
index 4cdba0f323..6f1fcf1011 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/terminal/HopGuiBottomDock.java
@@ -574,6 +574,15 @@ public class HopGuiBottomDock extends Composite implements
TabClosable {
unregisterTerminal(terminalId);
}
+ Object toolContent = tabItem.getData(DATA_TOOL_CONTENT);
+ if (toolContent instanceof Control content && !content.isDisposed()) {
+ content.dispose();
+ }
+ Control tabControl = tabItem.getControl();
+ if (tabControl != null && !tabControl.isDisposed()) {
+ tabControl.dispose();
+ }
+
tabItem.dispose();
}
diff --git
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
index aadc58001a..bba654a925 100644
---
a/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
+++
b/ui/src/main/resources/org/apache/hop/ui/hopgui/perspective/database/messages/messages_en_US.properties
@@ -47,6 +47,7 @@ DatabasePerspective.ConnectToExecute.Title=Connect to
database?
DatabasePerspective.ConnectToExecute.Message=You are not connected to ''{0}''.
Connect now to execute this SQL?
DatabasePerspective.ConnectToExecute.Toggle=Don't ask again, change this
option in the configuration perspective
DatabasePerspective.SqlTab.Cancelled=Cancelled
+DatabasePerspective.SqlTab.Failed=Failed
DatabasePerspective.SqlTab.QueryRows=Query {0}: {1} row(s)
DatabasePerspective.SqlTab.QueryRowsLimited=Query {0}: {1} row(s). Results are
capped at {2} rows by the Database perspective (a SQL LIMIT above this value is
ignored). Change the query row limit under Configuration > Plugins > Database
Perspective.
DatabasePerspective.SqlTab.Executed=OK: {0}
@@ -72,6 +73,7 @@ DatabasePerspective.Operations.Status.Done=Done
DatabasePerspective.Operations.Status.Failed=Failed
DatabasePerspective.Operations.Status.Cancelled=Cancelled
DatabasePerspective.Operations.Kill.Tooltip=Cancel the selected running
operation
+DatabasePerspective.Operations.Clear.Tooltip=Remove finished operations from
the list
DatabasePerspective.Operations.KillCurrent.Tooltip=Cancel the current running
operation
DatabasePerspective.Operations.Minimize.Tooltip=Show a one-line status instead
of the operations list
DatabasePerspective.Operations.Expand.Tooltip=Show the operations list
diff --git
a/ui/src/test/java/org/apache/hop/ui/core/dialog/RowPreviewSupportTest.java
b/ui/src/test/java/org/apache/hop/ui/core/dialog/RowPreviewSupportTest.java
new file mode 100644
index 0000000000..0c0519a1b5
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/dialog/RowPreviewSupportTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.dialog;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.row.value.ValueMetaBinary;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(RestoreHopEngineEnvironmentExtension.class)
+class RowPreviewSupportTest {
+
+ @BeforeAll
+ static void initHop() throws Exception {
+ HopEnvironment.init();
+ }
+
+ @Test
+ void binaryCellIsHexEncodedUnlessAvoided() throws Exception {
+ ValueMetaBinary meta = new ValueMetaBinary("payload");
+ byte[] bytes = new byte[] {0x0a, 0x0b};
+ assertEquals("0a0b", RowPreviewSupport.formatCell(meta, bytes, false));
+ assertEquals(meta.getString(bytes), RowPreviewSupport.formatCell(meta,
bytes, true));
+ }
+
+ @Test
+ void nullBinaryIsNull() throws Exception {
+ ValueMetaBinary meta = new ValueMetaBinary("payload");
+ assertNull(RowPreviewSupport.formatCell(meta, null, false));
+ }
+
+ @Test
+ void stringCellUsesValueMeta() throws Exception {
+ ValueMetaString meta = new ValueMetaString("name");
+ assertEquals("Ada", RowPreviewSupport.formatCell(meta, "Ada", false));
+ }
+
+ @Test
+ void formatColumnMetaTooltipDelegates() {
+ ValueMetaString meta = new ValueMetaString("customer_name");
+ meta.setLength(100);
+ String tip = RowPreviewSupport.formatColumnMetaTooltip(meta);
+ assertTrue(tip.contains("customer_name"));
+ assertTrue(tip.contains("100"));
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/delegates/FileRefreshHandlerMapTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/delegates/FileRefreshHandlerMapTest.java
new file mode 100644
index 0000000000..e58161a1dd
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/delegates/FileRefreshHandlerMapTest.java
@@ -0,0 +1,56 @@
+/*
+ * 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.hopgui.delegates;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
+import org.junit.jupiter.api.Test;
+
+class FileRefreshHandlerMapTest {
+
+ @Test
+ void twoHandlersShareAWatchUntilTheLastIsRemoved() {
+ FileRefreshHandlerMap map = new FileRefreshHandlerMap();
+ EmptyHopFileTypeHandler explorer = new EmptyHopFileTypeHandler();
+ EmptyHopFileTypeHandler database = new EmptyHopFileTypeHandler();
+
+ assertTrue(map.add("file:///tmp/q.sql", explorer));
+ assertFalse(map.add("file:///tmp/q.sql", database));
+ map.alias("file:///tmp/q.sql", "/tmp/q.sql");
+ assertEquals(2, map.get("file:///tmp/q.sql").size());
+ assertEquals(2, map.get("/tmp/q.sql").size());
+
+ assertFalse(map.remove("/tmp/q.sql", database));
+ assertEquals(1, map.get("file:///tmp/q.sql").size());
+ assertTrue(map.remove("file:///tmp/q.sql", explorer));
+ assertTrue(map.get("file:///tmp/q.sql").isEmpty());
+ assertTrue(map.get("/tmp/q.sql").isEmpty());
+ }
+
+ @Test
+ void removeAllDropsEveryHandler() {
+ FileRefreshHandlerMap map = new FileRefreshHandlerMap();
+ map.add("a.sql", new EmptyHopFileTypeHandler());
+ map.add("a.sql", new EmptyHopFileTypeHandler());
+ assertTrue(map.removeAll("a.sql"));
+ assertTrue(map.get("a.sql").isEmpty());
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
index 812af83180..87d8df2e86 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseOperationsPanelTest.java
@@ -18,8 +18,11 @@
package org.apache.hop.ui.hopgui.perspective.database;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import org.apache.hop.core.database.Database;
import org.junit.jupiter.api.Test;
class DatabaseOperationsPanelTest {
@@ -40,7 +43,25 @@ class DatabaseOperationsPanelTest {
operation.fail("boom");
String line = DatabaseOperationsPanel.formatStatusLine(operation);
assertTrue(line.startsWith("Connect - Failed - "));
- assertTrue(line.endsWith(" ms") || line.contains(" s"));
+ assertTrue(line.contains("boom"));
+ assertTrue(line.endsWith(" ms") || line.contains(" s") ||
line.contains("boom"));
+ }
+
+ @Test
+ void trimFinishedKeepsNewestFinishedAndAllRunning() {
+ java.util.List<DatabaseOperation> operations = new java.util.ArrayList<>();
+ DatabaseOperation running = new DatabaseOperation("run", "db");
+ operations.add(running);
+ for (int i = 0; i < 5; i++) {
+ DatabaseOperation done = new DatabaseOperation("done-" + i, "db");
+ done.complete();
+ operations.add(done);
+ }
+ DatabaseOperationsPanel.trimFinished(operations, 2);
+ assertEquals(3, operations.size());
+ assertEquals(running, operations.get(0));
+ assertEquals("done-0", operations.get(1).getDescription());
+ assertEquals("done-1", operations.get(2).getDescription());
}
@Test
@@ -48,6 +69,14 @@ class DatabaseOperationsPanelTest {
assertEquals("", DatabaseOperationsPanel.formatStatusLine(null));
}
+ @Test
+ void failDetachesDatabase() {
+ DatabaseOperation operation = new DatabaseOperation("Connect", "shop");
+ operation.attachDatabase(mock(Database.class));
+ operation.fail("boom");
+ assertNull(operation.getDatabase().get());
+ }
+
@Test
void formatElapsed() {
assertEquals("0 ms", DatabaseOperationsPanel.formatElapsed(0));
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchPreviewSqlTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchPreviewSqlTest.java
index c4496acfe8..15a246cff3 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchPreviewSqlTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchPreviewSqlTest.java
@@ -65,4 +65,18 @@ class DatabaseWorkbenchPreviewSqlTest {
assertEquals(
"SELECT * FROM t", DatabaseWorkbench.previewSelectSql(meta, variables,
null, "t", 1000));
}
+
+ @Test
+ void previewSelectSqlUsesPrefixLimitClause() {
+ DatabaseMeta meta = mock(DatabaseMeta.class);
+ IVariables variables = mock(IVariables.class);
+ when(meta.getQuotedSchemaTableCombination(any(), eq("dbo"), eq("mytable")))
+ .thenReturn("dbo.mytable");
+ when(meta.getLimitClausePrefix(1000)).thenReturn(" TOP 1000");
+ when(meta.getLimitClause(1000)).thenReturn("");
+
+ assertEquals(
+ "SELECT TOP 1000 * FROM dbo.mytable",
+ DatabaseWorkbench.previewSelectSql(meta, variables, "dbo", "mytable",
1000));
+ }
}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViewsTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViewsTest.java
index 630096ab75..9b857ba218 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViewsTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseWorkbenchViewsTest.java
@@ -34,6 +34,11 @@ class DatabaseWorkbenchViewsTest {
assertFalse(DatabaseWorkbenchViews.isDockOpen(null));
}
+ @Test
+ void dialogIsNotOpenWithoutHopGui() {
+ assertFalse(DatabaseWorkbenchViews.isDialogOpen(null));
+ }
+
@Test
void openDialogAndDockTolerateNullHopGui() {
assertDoesNotThrow(() -> DatabaseWorkbenchViews.openDialog(null));