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 39bff32a37 Database join performance improvement, fixes #4136 (#7834)
39bff32a37 is described below

commit 39bff32a3738a09790c7a9b4e84b0042739b4bed
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Fri Aug 7 20:33:49 2026 +0200

    Database join performance improvement, fixes #4136 (#7834)
---
 .../org/apache/hop/core/database/Database.java     | 41 +++++++++++-
 .../org/apache/hop/core/database/DatabaseTest.java | 75 ++++++++++++++++++++++
 .../transforms/databasejoin/DatabaseJoin.java      |  1 -
 3 files changed, 113 insertions(+), 4 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 b64ddc6b05..452c01cd7f 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
@@ -147,6 +147,16 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
 
   private IRowMeta rowMeta;
 
+  /**
+   * The result row metadata derived by {@link #openQuery(PreparedStatement, 
IRowMeta, Object[])},
+   * cached together with the prepared statement it was derived from. A 
prepared statement always
+   * returns the same result layout, so transforms which re-execute the same 
statement once per
+   * incoming row (Database Join) do not have to rebuild it for every single 
row.
+   */
+  private PreparedStatement openQueryStatement;
+
+  private IRowMeta openQueryRowMeta;
+
   private int written;
 
   private final ILogChannel log;
@@ -647,6 +657,8 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
 
   /** Disconnect from the database and close all open prepared statements. */
   public synchronized void disconnect() {
+    openQueryStatement = null;
+    openQueryRowMeta = null;
     if (connection == null) {
       return; // Nothing to do...
     }
@@ -995,6 +1007,7 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
 
   public void closePreparedStatement(PreparedStatement ps) throws 
HopDatabaseException {
     if (ps != null) {
+      forgetOpenQueryRowMeta(ps);
       try {
         ps.close();
       } catch (SQLException e) {
@@ -1003,6 +1016,18 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
     }
   }
 
+  /**
+   * Drop the cached {@link #openQueryRowMeta} when the statement it was 
derived from is closed. A
+   * driver is free to hand out the same statement object again for different 
SQL afterwards, so the
+   * cached layout must not survive it.
+   */
+  private void forgetOpenQueryRowMeta(PreparedStatement ps) {
+    if (ps == openQueryStatement) {
+      openQueryStatement = null;
+      openQueryRowMeta = null;
+    }
+  }
+
   public void closeInsert() throws HopDatabaseException {
     if (prepStatementInsert != null) {
       try {
@@ -1821,9 +1846,18 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
       // to get the length of a String field. So, on MySQL, we ignore the 
length
       // of Strings in result rows.
       //
-      log.snap(Metrics.METRIC_DATABASE_GET_ROW_META_START, 
databaseMeta.getName());
-      rowMeta = getRowInfo(res.getMetaData(), databaseMeta.isMySqlVariant(), 
false);
-      log.snap(Metrics.METRIC_DATABASE_GET_ROW_META_STOP, 
databaseMeta.getName());
+      // A prepared statement always returns the same result layout, so the 
row metadata only has to
+      // be derived the first time we see this statement. Rebuilding it on 
every execution costs one
+      // plugin class load per column and dominates transforms which execute 
the statement once per
+      // incoming row, such as Database Join.
+      //
+      if (ps != openQueryStatement || openQueryRowMeta == null) {
+        log.snap(Metrics.METRIC_DATABASE_GET_ROW_META_START, 
databaseMeta.getName());
+        openQueryRowMeta = getRowInfo(res.getMetaData(), 
databaseMeta.isMySqlVariant(), false);
+        openQueryStatement = ps;
+        log.snap(Metrics.METRIC_DATABASE_GET_ROW_META_STOP, 
databaseMeta.getName());
+      }
+      rowMeta = openQueryRowMeta;
     } catch (SQLException ex) {
       throw new HopDatabaseException("ERROR executing query", ex);
     } catch (Exception e) {
@@ -2466,6 +2500,7 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
         selStmt = null;
       }
       if (pstmt != null) {
+        forgetOpenQueryRowMeta(pstmt);
         pstmt.close();
         pstmt = null;
       }
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 77b33eaf3c..6cceafc489 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
@@ -56,7 +56,9 @@ import org.apache.hop.core.logging.ILoggingObject;
 import org.apache.hop.core.logging.LogLevel;
 import org.apache.hop.core.row.IRowMeta;
 import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
 import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.util.TestUtil;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.variables.Variables;
@@ -624,4 +626,77 @@ class DatabaseTest {
     assertEquals(columnName, iRowMeta.getValueMeta(0).getName());
     assertInstanceOf(ValueMetaNumber.class, iRowMeta.getValueMeta(0));
   }
+
+  @Test
+  void testOpenQueryDerivesRowMetaOncePerPreparedStatement() throws Exception {
+    Database db = mockDatabaseForOpenQuery();
+
+    // Database Join re-executes the same prepared statement once per incoming 
row. Deriving the
+    // result layout costs one plugin class load per column, so it has to 
happen only once.
+    //
+    for (int i = 0; i < 5; i++) {
+      db.openQuery(ps, new RowMeta(), new Object[] {});
+
+      assertEquals(1, db.getReturnRowMeta().size());
+      assertEquals(columnName, 
db.getReturnRowMeta().getValueMeta(0).getName());
+      assertInstanceOf(ValueMetaNumber.class, 
db.getReturnRowMeta().getValueMeta(0));
+    }
+
+    verify(rs, times(1)).getMetaData();
+  }
+
+  @Test
+  void testOpenQueryDerivesRowMetaAgainAfterStatementIsClosed() throws 
Exception {
+    Database db = mockDatabaseForOpenQuery();
+
+    db.openQuery(ps, new RowMeta(), new Object[] {});
+    db.closePreparedStatement(ps);
+    db.openQuery(ps, new RowMeta(), new Object[] {});
+
+    // A driver may hand the same statement object back for different SQL once 
it is closed, so the
+    // cached layout must not survive it.
+    verify(rs, times(2)).getMetaData();
+  }
+
+  @Test
+  void testOpenQueryDerivesRowMetaPerStatementWhenStatementsAreAlternated() 
throws Exception {
+    Database db = mockDatabaseForOpenQuery();
+
+    // A second statement on the same connection, returning a different layout.
+    PreparedStatement otherPs = mock(PreparedStatement.class);
+    ResultSet otherRs = mock(ResultSet.class);
+    ResultSetMetaData otherRsMetaData = mock(ResultSetMetaData.class);
+    when(otherRsMetaData.getColumnCount()).thenReturn(1);
+    when(otherRsMetaData.getColumnName(1)).thenReturn("bonus");
+    when(otherRsMetaData.getColumnLabel(1)).thenReturn("bonus");
+    when(otherRsMetaData.getColumnType(1)).thenReturn(Types.VARCHAR);
+    when(otherRs.getMetaData()).thenReturn(otherRsMetaData);
+    when(otherPs.executeQuery()).thenReturn(otherRs);
+
+    // Alternating statements must never hand out the layout cached for the 
other one.
+    //
+    for (int i = 0; i < 3; i++) {
+      db.openQuery(ps, new RowMeta(), new Object[] {});
+      assertEquals(columnName, 
db.getReturnRowMeta().getValueMeta(0).getName());
+      assertInstanceOf(ValueMetaNumber.class, 
db.getReturnRowMeta().getValueMeta(0));
+
+      db.openQuery(otherPs, new RowMeta(), new Object[] {});
+      assertEquals("bonus", db.getReturnRowMeta().getValueMeta(0).getName());
+      assertInstanceOf(ValueMetaString.class, 
db.getReturnRowMeta().getValueMeta(0));
+    }
+  }
+
+  private Database mockDatabaseForOpenQuery() throws SQLException {
+    when(rsMetaData.getColumnCount()).thenReturn(1);
+    when(rsMetaData.getColumnName(1)).thenReturn(columnName);
+    when(rsMetaData.getColumnLabel(1)).thenReturn(columnName);
+    when(rsMetaData.getColumnType(1)).thenReturn(Types.DECIMAL);
+    when(rs.getMetaData()).thenReturn(rsMetaData);
+    when(ps.executeQuery()).thenReturn(rs);
+    when(meta.getIDatabase()).thenReturn(new NoneDatabaseMeta());
+
+    Database db = new Database(log, variables, meta);
+    db.setConnection(conn);
+    return db;
+  }
 }
diff --git 
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoin.java
 
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoin.java
index 18b3ad35b3..f4cca36ee9 100644
--- 
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoin.java
+++ 
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoin.java
@@ -96,7 +96,6 @@ public class DatabaseJoin extends 
BaseTransform<DatabaseJoinMeta, DatabaseJoinDa
         
data.lookupRowMeta.addValueMeta(rowMeta.getValueMeta(data.keynrs[i]).clone());
       }
     }
-    final ResultSet rs;
     try {
       // Construct the parameters row...
       Object[] lookupRowData = new Object[data.lookupRowMeta.size()];

Reply via email to