Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to Data Hopper EDW (formerly hop-datavault) are documented i

## Unreleased

### Import tables shows progress (issue #134)

- Catalog **Import database tables**, source-model **Import schema**, and dimensional **Import database tables** run JDBC listing and per-table import under Hop's cancelable progress dialog (wait cursor on Hop Web)
- Cancel stops before the next table; already-written catalog records and imported canvas tables are kept
- CSV / Parquet / Iceberg catalog import shows a wait cursor while discovering the file or table schema

### Partition large BV SCD2 loads (issue #141)

- SCD2 table option **Hash-key partitions** (None / 4 / 8 / 16) splits a Full rebuild so each satellite `ORDER BY` covers a first-byte slice of the parent hash key
Expand Down
2 changes: 2 additions & 0 deletions docs/help/import-database-tables-catalog-dialog.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ Related guide: link:../datavault-source-database.adoc[datavault-source-database]
Select a Hop database connection and tables to import as catalog `DV_SOURCE` record definitions (or onto a source model).

Filter by schema, multi-select tables, and optionally import primary/foreign keys. Existing catalog names are not overwritten without confirmation.

Listing tables and importing selected tables each show a progress dialog (table name as the current step). Cancel stops before the next table; records already written to the catalog are kept. On Hop Web, a wait cursor is shown instead of a progress bar.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.apache.hop.ui.hopgui.context.GuiContextUtil;
import org.apache.hop.ui.hopgui.context.IGuiContextHandler;
import org.hopper.edw.catalog.hopgui.perspective.DataCatalogPerspective;
import org.hopper.edw.datavault.hopgui.GuiBusySupport;
import org.hopper.edw.datavault.metadata.DataVaultModel;

/** Shows the import menu for the Data Catalog perspective. */
Expand Down Expand Up @@ -88,15 +87,12 @@ private static List<GuiAction> buildActions(DataCatalogImportContext context) {
label,
tooltip,
image,
(shiftClicked, controlClicked, parameters) ->
GuiBusySupport.showWhile(
context.getShell(),
() -> {
importer.execute(context);
if (context.getOnComplete() != null) {
context.getOnComplete().run();
}
})));
(shiftClicked, controlClicked, parameters) -> {
importer.execute(context);
if (context.getOnComplete() != null) {
context.getOnComplete().run();
}
}));
}
return actions;
}
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/org/hopper/edw/datavault/hopgui/GuiBusySupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package org.hopper.edw.datavault.hopgui;

import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.widgets.Control;
Expand All @@ -26,6 +28,31 @@ public final class GuiBusySupport {

private GuiBusySupport() {}

/**
* Runs {@code callable} under the wait cursor and returns its value. Checked exceptions from
* {@code callable} are rethrown to the caller after the cursor is restored.
*/
public static <T> T callWhile(Control control, Callable<T> callable) throws Exception {
if (callable == null) {
return null;
}
AtomicReference<T> value = new AtomicReference<>();
AtomicReference<Exception> error = new AtomicReference<>();
showWhile(
control,
() -> {
try {
value.set(callable.call());
} catch (Exception e) {
error.set(e);
}
});
if (error.get() != null) {
throw error.get();
}
return value.get();
}

public static void showWhile(Control control, Runnable runnable) {
if (runnable == null) {
return;
Expand Down
188 changes: 188 additions & 0 deletions src/main/java/org/hopper/edw/datavault/hopgui/GuiProgressSupport.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* Copyright 2026 i-Bridge bv
*
* Licensed 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.hopper.edw.datavault.hopgui;

import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.hop.core.IProgressMonitor;
import org.apache.hop.core.ProgressNullMonitorListener;
import org.apache.hop.i18n.BaseMessages;
import org.apache.hop.ui.core.dialog.ErrorDialog;
import org.apache.hop.ui.core.dialog.ProgressMonitorDialog;
import org.apache.hop.ui.util.EnvironmentUtils;
import org.eclipse.swt.widgets.Shell;

/**
* Runs long work under Hop's {@link ProgressMonitorDialog} (desktop) or a wait cursor (Hop Web /
* missing shell).
*
* <p>{@link ProgressMonitorDialog}'s monitor {@code done()} disposes the dialog and unblocks the UI
* thread. Callers must not invoke {@code done()} themselves. This helper defers {@code done()}
* until the worker return value is stored, matching {@code ModelDialogValidationSupport}.
*/
public final class GuiProgressSupport {

private static final Class<?> PKG = GuiProgressSupport.class;

private GuiProgressSupport() {}

/** Work that reports progress and may throw checked exceptions. */
@FunctionalInterface
public interface ProgressWork<T> {
T run(IProgressMonitor monitor) throws Exception;
}

/**
* Result of {@link #run(Shell, boolean, ProgressWork)}, including whether the user cancelled.
*
* <p>{@code value} is {@code null} when work threw (an error dialog is already shown) or when
* there was no work.
*/
public record ProgressResult<T>(T value, boolean cancelled) {}

/**
* Runs {@code work} with a cancelable progress dialog on desktop Hop. On Hop Web, or when {@code
* shell} is missing, falls back to {@link GuiBusySupport} and a null monitor.
*/
public static <T> ProgressResult<T> run(Shell shell, boolean cancelable, ProgressWork<T> work) {
if (work == null) {
return new ProgressResult<>(null, false);
}
if (shell == null || shell.isDisposed() || EnvironmentUtils.getInstance().isWeb()) {
return runWithWaitCursor(shell, work);
}

AtomicReference<T> value = new AtomicReference<>();
ProgressMonitorDialog monitorDialog = new ProgressMonitorDialog(shell);
try {
monitorDialog.run(
cancelable,
monitor -> {
DeferredDoneMonitor deferred = new DeferredDoneMonitor(monitor);
try {
value.set(work.run(deferred));
} catch (Throwable e) {
throw new InvocationTargetException(
e,
BaseMessages.getString(
PKG, "GuiProgressSupport.Error.Exception", e.getMessage()));
} finally {
deferred.finish();
}
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
new ErrorDialog(
shell,
BaseMessages.getString(PKG, "GuiProgressSupport.Error.Title"),
BaseMessages.getString(PKG, "GuiProgressSupport.Error.Message"),
e);
return new ProgressResult<>(null, false);
} catch (Exception e) {
new ErrorDialog(
shell,
BaseMessages.getString(PKG, "GuiProgressSupport.Error.Title"),
BaseMessages.getString(PKG, "GuiProgressSupport.Error.Message"),
e);
return new ProgressResult<>(null, false);
}

boolean cancelled =
monitorDialog.getProgressMonitor() != null
&& monitorDialog.getProgressMonitor().isCanceled();
return new ProgressResult<>(value.get(), cancelled);
}

private static <T> ProgressResult<T> runWithWaitCursor(Shell shell, ProgressWork<T> work) {
AtomicReference<T> value = new AtomicReference<>();
AtomicReference<Exception> error = new AtomicReference<>();
Runnable body =
() -> {
try {
value.set(work.run(new ProgressNullMonitorListener()));
} catch (Exception e) {
error.set(e);
}
};
if (shell != null && !shell.isDisposed()) {
GuiBusySupport.showWhile(shell, body);
} else {
body.run();
}
if (error.get() != null) {
if (shell != null && !shell.isDisposed()) {
new ErrorDialog(
shell,
BaseMessages.getString(PKG, "GuiProgressSupport.Error.Title"),
BaseMessages.getString(PKG, "GuiProgressSupport.Error.Message"),
error.get());
}
return new ProgressResult<>(null, false);
}
return new ProgressResult<>(value.get(), false);
}

/**
* Forwards progress updates but defers {@link #done()} so the progress shell is not disposed
* until the caller has stored the worker result.
*/
private static final class DeferredDoneMonitor implements IProgressMonitor {
private final IProgressMonitor delegate;
private boolean finished;

private DeferredDoneMonitor(IProgressMonitor delegate) {
this.delegate = delegate != null ? delegate : new ProgressNullMonitorListener();
}

@Override
public void beginTask(String message, int nrWorks) {
delegate.beginTask(message, nrWorks);
}

@Override
public void subTask(String message) {
delegate.subTask(message);
}

@Override
public boolean isCanceled() {
return delegate.isCanceled();
}

@Override
public void worked(int nrWorks) {
delegate.worked(nrWorks);
}

@Override
public void done() {
// Deferred — see finish().
}

@Override
public void setTaskName(String taskName) {
delegate.setTaskName(taskName);
}

private void finish() {
if (finished) {
return;
}
finished = true;
delegate.done();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@
import java.util.ArrayList;
import java.util.List;
import org.apache.hop.core.Const;
import org.apache.hop.core.database.Database;
import org.apache.hop.core.database.DatabaseMeta;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.logging.ILoggingObject;
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.row.IValueMeta;
import org.apache.hop.core.row.RowMeta;
Expand All @@ -38,6 +34,8 @@
import org.apache.hop.ui.hopgui.HopGui;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Shell;
import org.hopper.edw.datavault.hopgui.GuiProgressSupport;
import org.hopper.edw.datavault.metadata.database.DvDatabaseSourceImportSupport;
import org.hopper.edw.datavault.metadata.dimensional.DimensionalModel;
import org.hopper.edw.datavault.metadata.dimensional.IDmTable;
import org.hopper.edw.datavault.metadata.dimensional.dbimport.DmDatabaseImportOptions;
Expand Down Expand Up @@ -102,23 +100,45 @@ public static void importDatabaseTables(
return;
}

GuiProgressSupport.ProgressResult<DmDatabaseImportResult> progress =
GuiProgressSupport.run(
shell,
true,
monitor ->
DmDatabaseTableImportSupport.importTables(
model,
databaseMeta,
options,
selectedTables,
variables,
metadataProvider,
monitor));
if (progress.value() == null) {
return;
}
DmDatabaseImportResult result = progress.value();
try {
DmDatabaseImportResult result =
DmDatabaseTableImportSupport.importTables(
model, databaseMeta, options, selectedTables, variables, metadataProvider);
for (IDmTable table : result.getImportedTablesOrEmpty()) {
model.getTables().add(table);
}
if (onChanged != null) {
onChanged.run();
}

StringBuilder message =
new StringBuilder(
BaseMessages.getString(
PKG,
"HopGuiDmDatabaseImportSupport.Success.Message",
result.getImportedTablesOrEmpty().size()));
StringBuilder message = new StringBuilder();
if (progress.cancelled()) {
message.append(
BaseMessages.getString(
PKG,
"HopGuiDmDatabaseImportSupport.Cancelled.Message",
result.getImportedTablesOrEmpty().size()));
message.append(Const.CR).append(Const.CR);
}
message.append(
BaseMessages.getString(
PKG,
"HopGuiDmDatabaseImportSupport.Success.Message",
result.getImportedTablesOrEmpty().size()));

if (!result.getWarningsOrEmpty().isEmpty()) {
message.append(Const.CR).append(Const.CR);
Expand Down Expand Up @@ -159,25 +179,19 @@ public static void importDatabaseTables(
private static List<String> promptForTableSelection(
Shell shell, IVariables variables, DatabaseMeta databaseMeta, DmDatabaseImportOptions options)
throws HopException {
String schemaName = variables.resolve(options.getSchemaName());
String[] tableNames;
ILoggingObject loggingObject =
new SimpleLoggingObject("DmDatabaseTableImport", LoggingObjectType.GENERAL, null);
try (Database database = new Database(loggingObject, variables, databaseMeta)) {
database.connect();
tableNames = database.getTablenames(schemaName, false);
} catch (Exception e) {
new ErrorDialog(
shell,
BaseMessages.getString(
PKG, "ImportDmDatabaseTablesOptionsDialog.ErrorListingTables.Title"),
BaseMessages.getString(
PKG, "ImportDmDatabaseTablesOptionsDialog.ErrorListingTables.Message"),
e);
GuiProgressSupport.ProgressResult<String[]> listed =
GuiProgressSupport.run(
shell,
true,
monitor ->
DvDatabaseSourceImportSupport.listTableNames(
databaseMeta, variables, options.getSchemaName(), monitor));
if (listed.cancelled() || listed.value() == null) {
return null;
}
String[] tableNames = listed.value();

if (tableNames == null || tableNames.length == 0) {
if (tableNames.length == 0) {
MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_INFORMATION);
mb.setText(
BaseMessages.getString(PKG, "ImportDmDatabaseTablesOptionsDialog.NoTablesFound.Title"));
Expand Down
Loading
Loading