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 1d1105ef80 Fix #6472: map SQLite column types by affinity, not driver 
JDBC type (#7564)
1d1105ef80 is described below

commit 1d1105ef80745f0bfcfa13399c451390fc8edbee
Author: Sergio Ramazzina <[email protected]>
AuthorDate: Sat Jul 18 21:08:36 2026 +0200

    Fix #6472: map SQLite column types by affinity, not driver JDBC type (#7564)
---
 .../hop/databases/sqlite/SqliteDatabaseMeta.java   |  58 ++++++++++
 .../databases/sqlite/SqliteDatabaseMetaTest.java   | 127 +++++++++++++++++++++
 2 files changed, 185 insertions(+)

diff --git 
a/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
 
b/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
index 9f9cfc21c6..e935b8874b 100644
--- 
a/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
+++ 
b/plugins/databases/sqlite/src/main/java/org/apache/hop/databases/sqlite/SqliteDatabaseMeta.java
@@ -17,14 +17,19 @@
 
 package org.apache.hop.databases.sqlite;
 
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.Locale;
 import org.apache.hop.core.Const;
 import org.apache.hop.core.database.BaseDatabaseMeta;
 import org.apache.hop.core.database.DatabaseMeta;
 import org.apache.hop.core.database.DatabaseMetaPlugin;
 import org.apache.hop.core.database.DriverDownload;
 import org.apache.hop.core.database.IDatabase;
+import org.apache.hop.core.exception.HopPluginException;
 import org.apache.hop.core.gui.plugin.GuiPlugin;
 import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaFactory;
 
 /** Contains SQLite specific information through static final members */
 @DatabaseMetaPlugin(
@@ -212,4 +217,57 @@ public class SqliteDatabaseMeta extends BaseDatabaseMeta 
implements IDatabase {
   public boolean isSqliteVariant() {
     return true;
   }
+
+  /**
+   * SQLite uses dynamic typing with name-based type affinity (see
+   * https://www.sqlite.org/datatype3.html section 3.1). The JDBC driver 
reports columns whose
+   * declared type carries a size preceded by a space (e.g. {@code "TEXT 
(50)"}) as {@link
+   * java.sql.Types#NUMERIC}, which makes the generic mapper turn them into 
BigNumber/Integer. Since
+   * the declared size is meaningless in SQLite, re-derive the Hop type from 
the affinity of the
+   * declared type name (which the driver still reports correctly), correcting 
only the columns the
+   * generic mapper got wrong. See issue #6472.
+   */
+  @Override
+  public IValueMeta customizeValueFromSqlType(IValueMeta v, ResultSetMetaData 
rm, int index)
+      throws SQLException {
+    if (v == null || rm == null) {
+      return super.customizeValueFromSqlType(v, rm, index);
+    }
+    String typeName = rm.getColumnTypeName(index);
+    if (typeName == null) {
+      return super.customizeValueFromSqlType(v, rm, index);
+    }
+    typeName = typeName.toUpperCase(Locale.ROOT);
+
+    int targetType;
+    if (typeName.contains("INT")) {
+      // INTEGER affinity: SQLite integers are 64-bit, which fit a Hop Integer 
(Long).
+      targetType = IValueMeta.TYPE_INTEGER;
+    } else if (typeName.contains("CHAR")
+        || typeName.contains("CLOB")
+        || typeName.contains("TEXT")) {
+      // TEXT affinity.
+      targetType = IValueMeta.TYPE_STRING;
+    } else if (typeName.contains("REAL")
+        || typeName.contains("FLOA")
+        || typeName.contains("DOUB")) {
+      // REAL affinity.
+      targetType = IValueMeta.TYPE_NUMBER;
+    } else {
+      // NUMERIC and BLOB affinity: keep whatever the generic mapper produced.
+      return super.customizeValueFromSqlType(v, rm, index);
+    }
+
+    if (v.getType() == targetType) {
+      return super.customizeValueFromSqlType(v, rm, index);
+    }
+
+    try {
+      IValueMeta corrected = ValueMetaFactory.cloneValueMeta(v, targetType);
+      corrected.setPrecision(-1);
+      return corrected;
+    } catch (HopPluginException e) {
+      throw new SQLException(e);
+    }
+  }
 }
diff --git 
a/plugins/databases/sqlite/src/test/java/org/apache/hop/databases/sqlite/SqliteDatabaseMetaTest.java
 
b/plugins/databases/sqlite/src/test/java/org/apache/hop/databases/sqlite/SqliteDatabaseMetaTest.java
index 9413442462..243384757c 100644
--- 
a/plugins/databases/sqlite/src/test/java/org/apache/hop/databases/sqlite/SqliteDatabaseMetaTest.java
+++ 
b/plugins/databases/sqlite/src/test/java/org/apache/hop/databases/sqlite/SqliteDatabaseMetaTest.java
@@ -20,23 +20,54 @@ import static 
org.junit.jupiter.api.Assertions.assertArrayEquals;
 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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashMap;
+import java.util.Map;
 import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.database.DatabasePluginType;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.plugins.PluginRegistry;
+import org.apache.hop.core.row.IValueMeta;
 import org.apache.hop.core.row.value.ValueMetaBigNumber;
 import org.apache.hop.core.row.value.ValueMetaBinary;
 import org.apache.hop.core.row.value.ValueMetaBoolean;
 import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.core.row.value.ValueMetaFactory;
+import org.apache.hop.core.row.value.ValueMetaInteger;
 import org.apache.hop.core.row.value.ValueMetaInternetAddress;
 import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaPluginType;
 import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.row.value.ValueMetaTimestamp;
+import org.apache.hop.core.variables.Variables;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 
 class SqliteDatabaseMetaTest {
 
+  @RegisterExtension
+  static RestoreHopEngineEnvironmentExtension env = new 
RestoreHopEngineEnvironmentExtension();
+
   private SqliteDatabaseMeta nativeMeta;
 
+  @BeforeAll
+  static void setUpBeforeClass() throws HopException {
+    PluginRegistry.addPluginType(ValueMetaPluginType.getInstance());
+    PluginRegistry.addPluginType(DatabasePluginType.getInstance());
+    PluginRegistry.init();
+  }
+
   @BeforeEach
   void setupBefore() {
     nativeMeta = new SqliteDatabaseMeta();
@@ -146,4 +177,100 @@ class SqliteDatabaseMetaTest {
         nativeMeta.getFieldDefinition(
             new ValueMetaInternetAddress("FOO"), "", "", false, false, true));
   }
+
+  /**
+   * The SQLite JDBC driver reports columns whose declared type carries a size 
with a space before
+   * the parenthesis (e.g. "TEXT (50)") as {@link java.sql.Types#NUMERIC}, so 
the generic mapper
+   * wrongly turns them into BigNumber/Integer. The Hop type must instead 
follow SQLite's name-based
+   * type affinity. See issue #6472.
+   */
+  private ResultSetMetaData resultSetMetaWithTypeName(String typeName) throws 
SQLException {
+    ResultSetMetaData rm = mock(ResultSetMetaData.class);
+    when(rm.getColumnTypeName(1)).thenReturn(typeName);
+    return rm;
+  }
+
+  private IValueMeta effective(IValueMeta original, ResultSetMetaData rm) 
throws SQLException {
+    IValueMeta customized = nativeMeta.customizeValueFromSqlType(original, rm, 
1);
+    return customized != null ? customized : original;
+  }
+
+  @Test
+  void testTextAffinityStaysStringWhenDriverReportsNumeric() throws 
SQLException {
+    // "TEXT (50)" -> driver says NUMERIC -> generic mapper produced 
BigNumber(50)
+    IValueMeta generic = new ValueMetaBigNumber("TEXT_50", 50, 0);
+    assertTrue(effective(generic, 
resultSetMetaWithTypeName("TEXT")).isString());
+  }
+
+  @Test
+  void testIntegerAffinityStaysIntegerWhenDriverReportsNumeric() throws 
SQLException {
+    // "INTEGER (20)" -> driver says NUMERIC -> generic mapper produced 
BigNumber(20)
+    IValueMeta generic = new ValueMetaBigNumber("INTEGER20", 20, 0);
+    assertTrue(effective(generic, 
resultSetMetaWithTypeName("INTEGER")).isInteger());
+  }
+
+  @Test
+  void testRealAffinityStaysNumberWhenDriverReportsNumeric() throws 
SQLException {
+    // "REAL (10)" -> driver says NUMERIC -> generic mapper produced 
Integer(10)
+    IValueMeta generic = new ValueMetaInteger("REAL10", 10, 0);
+    assertTrue(effective(generic, 
resultSetMetaWithTypeName("REAL")).isNumber());
+  }
+
+  @Test
+  void testNumericAffinityKeepsGenericNumericMapping() throws SQLException {
+    // "NUMERIC (20)" is genuine NUMERIC affinity: keep the generic mapping 
(BigNumber)
+    IValueMeta generic = new ValueMetaBigNumber("NUMERIC20", 20, 0);
+    assertTrue(effective(generic, 
resultSetMetaWithTypeName("NUMERIC")).isBigNumber());
+  }
+
+  @Test
+  void testCorrectlyTypedStringIsNotAltered() throws SQLException {
+    // "TEXT" (no size) already maps to String: no correction needed
+    IValueMeta generic = new ValueMetaString("TEXT");
+    assertTrue(effective(generic, 
resultSetMetaWithTypeName("TEXT")).isString());
+  }
+
+  /**
+   * End-to-end reproduction of issue #6472 against the real SQLite JDBC 
driver, using the exact DDL
+   * from the report (sizes written with a space before the parenthesis). 
Drives the full
+   * ValueMetaBase.getValueFromSqlType -> customizeValueFromSqlType chain.
+   */
+  @Test
+  void testGetValueFromSqlTypeFollowsSqliteAffinity() throws Exception {
+    Class.forName("org.sqlite.JDBC");
+    Map<String, Integer> types = new HashMap<>();
+    try (Connection c = DriverManager.getConnection("jdbc:sqlite::memory:");
+        Statement st = c.createStatement()) {
+      st.execute(
+          "CREATE TABLE str_test ("
+              + "\"TEXT\" TEXT, TEXT_50 TEXT (50), "
+              + "\"INTEGER\" INTEGER, INTEGER20 INTEGER (20), "
+              + "\"NUMERIC\" NUMERIC, NUMERIC20 NUMERIC (20), "
+              + "\"REAL\" REAL, REAL10 REAL (10), \"BLOB\" BLOB)");
+      try (ResultSet rs = st.executeQuery("SELECT * FROM str_test")) {
+        ResultSetMetaData rm = rs.getMetaData();
+        DatabaseMeta dbMeta = new DatabaseMeta();
+        dbMeta.setIDatabase(new SqliteDatabaseMeta());
+        IValueMeta valueMetaBase = 
ValueMetaFactory.createValueMeta(IValueMeta.TYPE_NONE);
+        for (int i = 1; i <= rm.getColumnCount(); i++) {
+          IValueMeta v =
+              valueMetaBase.getValueFromSqlType(
+                  new Variables(), dbMeta, rm.getColumnName(i), rm, i, false, 
false);
+          types.put(rm.getColumnName(i), v.getType());
+        }
+      }
+    }
+
+    // Sized columns (the buggy cases) must follow SQLite name-based affinity.
+    assertEquals(IValueMeta.TYPE_STRING, types.get("TEXT_50"));
+    assertEquals(IValueMeta.TYPE_INTEGER, types.get("INTEGER20"));
+    assertEquals(IValueMeta.TYPE_NUMBER, types.get("REAL10"));
+    assertEquals(IValueMeta.TYPE_BIGNUMBER, types.get("NUMERIC20"));
+
+    // Unsized columns were already correct and must stay so.
+    assertEquals(IValueMeta.TYPE_STRING, types.get("TEXT"));
+    assertEquals(IValueMeta.TYPE_INTEGER, types.get("INTEGER"));
+    assertEquals(IValueMeta.TYPE_NUMBER, types.get("REAL"));
+    assertEquals(IValueMeta.TYPE_STRING, types.get("BLOB"));
+  }
 }

Reply via email to