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 7ab2b12669 issue #7251 : fine-tuning of DDL generation (#7287)
7ab2b12669 is described below

commit 7ab2b12669d90ce7fa2ec2325b421b65f661742a
Author: Matt Casters <[email protected]>
AuthorDate: Mon Jun 15 12:46:14 2026 +0200

    issue #7251 : fine-tuning of DDL generation (#7287)
    
    * issue #7251 : fine-tuning of DDL generation
    
    * issue #7251 : javadoc of compatibility variable HOP_DB_DDL_COMPATIBLE
---
 core/src/main/java/org/apache/hop/core/Const.java  |  14 +
 .../org/apache/hop/core/database/Database.java     | 315 +++++++++++++++++++--
 .../modules/ROOT/pages/database/databases.adoc     |  46 ++-
 3 files changed, 353 insertions(+), 22 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/Const.java 
b/core/src/main/java/org/apache/hop/core/Const.java
index 2769da1d21..27d72a05b5 100644
--- a/core/src/main/java/org/apache/hop/core/Const.java
+++ b/core/src/main/java/org/apache/hop/core/Const.java
@@ -1058,6 +1058,20 @@ public class Const {
           "Default JDBC statement query timeout in seconds for database query 
preview (0 = unset).")
   public static final String HOP_QUERY_PREVIEW_TIMEOUT = 
"HOP_QUERY_PREVIEW_TIMEOUT";
 
+  /**
+   * Apache Hop 2.18 and before (even before Hop) used an optimistic system 
where each value type is
+   * given a chance to interpret the database field data type. This was done 
to support special data
+   * types like GIS, and so on. If you set this variable to true the old 
system is used. If not Hop
+   * tries first to do an explicit mapping of the most comm data types before 
falling back to the
+   * old system.
+   */
+  @Variable(
+      scope = VariableScope.SYSTEM,
+      value = "false",
+      description =
+          "Make the data type detection in RDBMS connections compatible with 
2.18 and before.")
+  public static final String HOP_DB_DDL_COMPATIBLE = "HOP_DB_DDL_COMPATIBLE";
+
   /**
    * rounds double f to any number of places after decimal point Does 
arithmetic using BigDecimal
    * class to avoid integer overflow while rounding
diff --git a/core/src/main/java/org/apache/hop/core/database/Database.java 
b/core/src/main/java/org/apache/hop/core/database/Database.java
index 4a190dcac3..73001b73fc 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
@@ -35,6 +35,7 @@ import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.sql.Savepoint;
 import java.sql.Statement;
+import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -91,6 +92,7 @@ import org.apache.hop.core.row.value.ValueMetaNone;
 import org.apache.hop.core.row.value.ValueMetaNumber;
 import org.apache.hop.core.row.value.ValueMetaString;
 import org.apache.hop.core.row.value.ValueMetaTimestamp;
+import org.apache.hop.core.util.EnvUtil;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.core.variables.IVariables;
 import org.apache.hop.core.variables.Variables;
@@ -2398,38 +2400,43 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
     }
   }
 
+  private static boolean ddlCompatible =
+      Const.toBoolean(EnvUtil.getSystemProperty(Const.HOP_DB_DDL_COMPATIBLE, 
"false"));
+
   private IValueMeta getValueFromSqlType(
-      ResultSetMetaData rm, int i, boolean ignoreLength, boolean 
lazyConversion)
+      ResultSetMetaData rm, int columnIndex, boolean ignoreLength, boolean 
lazyConversion)
       throws HopDatabaseException, SQLException {
-    // TODO If we do lazy conversion, we need to find out about the encoding
-    //
-
-    // Extract the name from the result set meta data...
+    // Extract the name from the result set metadata...
     //
     String name;
     if (databaseMeta.isMySqlVariant()) {
-      name = 
databaseMeta.getIDatabase().getLegacyColumnName(getDatabaseMetaData(), rm, i);
+      name =
+          
databaseMeta.getIDatabase().getLegacyColumnName(getDatabaseMetaData(), rm, 
columnIndex);
     } else {
-      name = rm.getColumnName(i);
+      name = rm.getColumnName(columnIndex);
     }
 
     // Check the name, sometimes it's empty.
     //
     if (Utils.isEmpty(name) || Const.onlySpaces(name)) {
-      name = "Field" + (i + 1);
+      name = "Field" + (columnIndex + 1);
     }
 
-    // Ask all the value meta types if they want to handle the SQL type.
-    // The first to reply something gets the workflow...
-    //
-    IValueMeta valueMeta = null;
-    for (IValueMeta valueMetaClass : valueMetaPluginClasses) {
-      IValueMeta v =
-          valueMetaClass.getValueFromSqlType(
-              this, databaseMeta, name, rm, i, ignoreLength, lazyConversion);
-      if (v != null) {
-        valueMeta = v;
-        break;
+    IValueMeta valueMeta;
+    if (ddlCompatible) {
+      // Classic approach is to ask all value metadata data types for advice
+      // on which data type the database column maps to best.
+      //
+      valueMeta =
+          getDataTypeFromAllValueMetaLegacy(rm, columnIndex, ignoreLength, 
lazyConversion, name);
+    } else {
+      // Map obvious JDBC types to Hop data types first, then fall back to 
value meta plugins
+      // for specialized types (UUID, INET, JSON, and so on).
+      //
+      valueMeta = getDataTypeFromKnownSqlType(rm, columnIndex, ignoreLength, 
lazyConversion, name);
+      if (valueMeta == null) {
+        valueMeta =
+            getDataTypeFromAllValueMetaLegacy(rm, columnIndex, ignoreLength, 
lazyConversion, name);
       }
     }
 
@@ -2441,10 +2448,278 @@ public class Database implements IVariables, 
ILoggingObject, AutoCloseable {
         "Unable to handle database column '"
             + name
             + "', on column index "
-            + i
+            + columnIndex
             + " : not a handled data type");
   }
 
+  /**
+   * Map well-known JDBC {@link Types} values to Hop value metadata using 
hard-coded rules. Returns
+   * {@code null} when the SQL type is not one of the standard mappings so 
that specialized value
+   * meta plugins can handle it.
+   */
+  private IValueMeta getDataTypeFromKnownSqlType(
+      ResultSetMetaData rm,
+      int columnIndex,
+      boolean ignoreLength,
+      boolean lazyConversion,
+      String name)
+      throws HopDatabaseException, SQLException {
+    int sqlType = rm.getColumnType(columnIndex);
+    int valtype = IValueMeta.TYPE_NONE;
+    int length = -1;
+    int precision = -1;
+    boolean isClob = false;
+    boolean signed = false;
+    try {
+      signed = rm.isSigned(columnIndex);
+    } catch (Exception ignored) {
+      // This JDBC driver doesn't support the isSigned method.
+    }
+
+    switch (sqlType) {
+      case Types.CHAR,
+          Types.VARCHAR,
+          Types.NVARCHAR,
+          Types.LONGVARCHAR,
+          Types.NCHAR,
+          Types.LONGNVARCHAR:
+        valtype = IValueMeta.TYPE_STRING;
+        if (!ignoreLength) {
+          length = rm.getColumnDisplaySize(columnIndex);
+        }
+        break;
+
+      case Types.CLOB, Types.NCLOB:
+        valtype = IValueMeta.TYPE_STRING;
+        length = DatabaseMeta.CLOB_LENGTH;
+        isClob = true;
+        break;
+
+      case Types.BIGINT:
+        if (signed) {
+          valtype = IValueMeta.TYPE_INTEGER;
+          precision = 0;
+          length = 15;
+        } else {
+          valtype = IValueMeta.TYPE_BIGNUMBER;
+          precision = 0;
+          length = 16;
+        }
+        break;
+
+      case Types.INTEGER:
+        valtype = IValueMeta.TYPE_INTEGER;
+        precision = 0;
+        length = 9;
+        break;
+
+      case Types.SMALLINT:
+        valtype = IValueMeta.TYPE_INTEGER;
+        precision = 0;
+        length = 4;
+        break;
+
+      case Types.TINYINT:
+        valtype = IValueMeta.TYPE_INTEGER;
+        precision = 0;
+        length = 2;
+        break;
+
+      case Types.DECIMAL, Types.DOUBLE, Types.FLOAT, Types.REAL, Types.NUMERIC:
+        valtype = IValueMeta.TYPE_NUMBER;
+        length = rm.getPrecision(columnIndex);
+        precision = rm.getScale(columnIndex);
+        // Precision in the database means the number of significant digits.
+        // In Hop, it means the digits before the decimal.
+        //
+        if (length > 0 && length > precision) {
+          length -= precision;
+        }
+
+        if (length >= 126) {
+          length = -1;
+        }
+        if (precision >= 126) {
+          precision = -1;
+        }
+
+        if (sqlType == Types.DOUBLE || sqlType == Types.FLOAT || sqlType == 
Types.REAL) {
+          if (precision == 0) {
+            precision = -1;
+          }
+
+          if (databaseMeta.getIDatabase().isPostgresVariant()
+              && sqlType == Types.DOUBLE
+              && precision >= 16
+              && length >= 16) {
+            precision = -1;
+            length = -1;
+          }
+
+          if (databaseMeta.getIDatabase().isMySqlVariant() && precision >= 
length) {
+            precision = -1;
+            length = -1;
+          }
+
+          if (length > 15 || precision > 15) {
+            valtype = IValueMeta.TYPE_BIGNUMBER;
+          }
+        } else {
+          if (precision == 0) {
+            if (length <= 18 && length > 0) {
+              valtype = IValueMeta.TYPE_INTEGER;
+            } else if (length > 18) {
+              valtype = IValueMeta.TYPE_BIGNUMBER;
+            }
+          } else if (length > 15 || precision > 15) {
+            valtype = IValueMeta.TYPE_BIGNUMBER;
+          }
+        }
+
+        if (databaseMeta.getIDatabase().isPostgresVariant()
+            && sqlType == Types.NUMERIC
+            && length == 0
+            && precision == 0) {
+          valtype = IValueMeta.TYPE_BIGNUMBER;
+          length = -1;
+          precision = -1;
+        }
+
+        if (databaseMeta.getIDatabase().isOracleVariant()) {
+          if (precision == 0 && length == 38) {
+            valtype =
+                databaseMeta.getIDatabase().isStrictBigNumberInterpretation()
+                    ? IValueMeta.TYPE_BIGNUMBER
+                    : IValueMeta.TYPE_INTEGER;
+          }
+          if (precision <= 0 && length <= 0) {
+            valtype = IValueMeta.TYPE_BIGNUMBER;
+            length = -1;
+            precision = -1;
+          }
+        }
+        break;
+
+      case Types.TIMESTAMP:
+        if (databaseMeta.supportsTimestampDataType()) {
+          valtype = IValueMeta.TYPE_TIMESTAMP;
+          length = rm.getScale(columnIndex);
+        } else {
+          valtype = IValueMeta.TYPE_DATE;
+        }
+        break;
+
+      case Types.DATE, Types.TIME:
+        valtype = IValueMeta.TYPE_DATE;
+        if (databaseMeta.getIDatabase().isMySqlVariant()) {
+          String property =
+              
databaseMeta.getConnectionProperties(this).getProperty("yearIsDateType");
+          if (property != null
+              && property.equalsIgnoreCase("false")
+              && rm.getColumnTypeName(columnIndex).equalsIgnoreCase("YEAR")) {
+            valtype = IValueMeta.TYPE_INTEGER;
+            precision = 0;
+            length = 4;
+          }
+        }
+        if (databaseMeta.getIDatabase().isTeradataVariant()) {
+          precision = 1;
+        }
+        break;
+
+      case Types.BOOLEAN, Types.BIT:
+        valtype = IValueMeta.TYPE_BOOLEAN;
+        break;
+
+      case Types.BINARY, Types.BLOB, Types.VARBINARY, Types.LONGVARBINARY:
+        valtype = IValueMeta.TYPE_BINARY;
+
+        if (databaseMeta.isDisplaySizeTwiceThePrecision()
+            && (2 * rm.getPrecision(columnIndex)) == 
rm.getColumnDisplaySize(columnIndex)) {
+          length = rm.getPrecision(columnIndex);
+        } else if (databaseMeta.getIDatabase().isOracleVariant()
+            && (sqlType == Types.VARBINARY || sqlType == Types.LONGVARBINARY)) 
{
+          valtype = IValueMeta.TYPE_STRING;
+          length = rm.getColumnDisplaySize(columnIndex);
+        } else if (databaseMeta.isMySqlVariant()
+            && (sqlType == Types.VARBINARY || sqlType == Types.LONGVARBINARY)) 
{
+          length = -1;
+        } else if (databaseMeta.getIDatabase().isSqliteVariant()) {
+          valtype = IValueMeta.TYPE_STRING;
+        } else {
+          length = -1;
+        }
+        precision = -1;
+        break;
+
+      default:
+        return null;
+    }
+
+    try {
+      IValueMeta valueMeta = ValueMetaFactory.createValueMeta(name, valtype);
+      valueMeta.setLength(length);
+      valueMeta.setPrecision(precision);
+      valueMeta.setLargeTextField(isClob);
+
+      setOriginalColumnMetadata(valueMeta, rm, columnIndex, ignoreLength);
+
+      if (lazyConversion && valtype == IValueMeta.TYPE_STRING) {
+        valueMeta.setStorageType(IValueMeta.STORAGE_TYPE_BINARY_STRING);
+        IValueMeta storageMetaData =
+            ValueMetaFactory.cloneValueMeta(valueMeta, IValueMeta.TYPE_STRING);
+        storageMetaData.setStorageType(IValueMeta.STORAGE_TYPE_NORMAL);
+        valueMeta.setStorageMetadata(storageMetaData);
+      }
+
+      IValueMeta customized =
+          databaseMeta.getIDatabase().customizeValueFromSqlType(valueMeta, rm, 
columnIndex);
+      return customized == null ? valueMeta : customized;
+    } catch (Exception e) {
+      throw new HopDatabaseException(
+          "Error determining value metadata from SQL resultset metadata", e);
+    }
+  }
+
+  private void setOriginalColumnMetadata(
+      IValueMeta valueMeta, ResultSetMetaData rm, int columnIndex, boolean 
ignoreLength)
+      throws SQLException {
+    valueMeta.setComments(rm.getColumnLabel(columnIndex));
+    valueMeta.setOriginalColumnType(rm.getColumnType(columnIndex));
+    valueMeta.setOriginalColumnTypeName(rm.getColumnTypeName(columnIndex));
+
+    int originalPrecision = -1;
+    if (!ignoreLength) {
+      originalPrecision = rm.getPrecision(columnIndex);
+    }
+    valueMeta.setOriginalPrecision(originalPrecision);
+    valueMeta.setOriginalScale(rm.getScale(columnIndex));
+
+    boolean originalSigned = false;
+    try {
+      originalSigned = rm.isSigned(columnIndex);
+    } catch (Exception ignored) {
+      // This JDBC driver doesn't support the isSigned method.
+    }
+    valueMeta.setOriginalSigned(originalSigned);
+  }
+
+  private IValueMeta getDataTypeFromAllValueMetaLegacy(
+      ResultSetMetaData rm, int i, boolean ignoreLength, boolean 
lazyConversion, String name)
+      throws HopDatabaseException {
+    IValueMeta valueMeta = null;
+    for (IValueMeta valueMetaClass : valueMetaPluginClasses) {
+      IValueMeta v =
+          valueMetaClass.getValueFromSqlType(
+              this, databaseMeta, name, rm, i, ignoreLength, lazyConversion);
+      if (v != null) {
+        valueMeta = v;
+        break;
+      }
+    }
+    return valueMeta;
+  }
+
   public boolean absolute(ResultSet rs, int position) throws 
HopDatabaseException {
     try {
       return rs.absolute(position);
diff --git a/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
index e3d265e948..be9a58a4c4 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/database/databases.adoc
@@ -74,11 +74,12 @@ The options table contains a list of key/value pairs that 
can be added to your J
 
 For example, to add additional options to a MS SQL database, you can add 
options to achieve a JDBC URL like the one below. Apache Hop will take care of 
these properties in the background, there usually is no need to manually modify 
the JDBC URL (but you _can_, and in a `Generic` connection, you _have to_).
 
-```
+[source]
+----
 jdbc:sqlserver://localhost:1433;" +
      "databaseName=AdventureWorks;integratedSecurity=true;" +
      "encrypt=true;trustServerCertificate=true
-```
+----
 
 [%noheader, width="50%", cols="4,1"]
 |===
@@ -87,5 +88,46 @@ jdbc:sqlserver://localhost:1433;" +
 |trustServerCertificate|true
 |===
 
+== Hard-coded JDBC to Hop data type mappings
+
+Below are the mappings applied by Hop when converting from database column 
types to Hop data types when `HOP_DB_DDL_COMPATIBLE` is `false` (the default).
+
+[cols="2,2", options="header"]
+|===
+| JDBC type | Hop type
+
+| `CHAR`, `VARCHAR`, `NVARCHAR`, `LONGVARCHAR`, `NCHAR`, `LONGNVARCHAR`
+| String
+
+| `CLOB`, `NCLOB`
+| String (large text)
+
+| `INTEGER`, `SMALLINT`, `TINYINT`
+| Integer
+
+| `BIGINT` (signed)
+| Integer
+
+| `BIGINT` (unsigned)
+| BigNumber
+
+| `FLOAT`, `DOUBLE`, `REAL`, `DECIMAL`, `NUMERIC`
+| Number (with length/precision rules; BigNumber when needed)
+
+| `DATE`, `TIME`
+| Date
+
+| `TIMESTAMP`
+| Timestamp (or Date if the database does not support Timestamp)
+
+| `BOOLEAN`, `BIT`
+| Boolean
+
+| `BINARY`, `BLOB`, `VARBINARY`, `LONGVARBINARY`
+| Binary (with database-specific overrides)
+|===
+
+Unmapped types (for example `OTHER` for UUID or INET) return no match from the 
hard-coded path and fall through to legacy method of figuring out the data 
type, which was asking all the different value metadata plugin types for a 
guess.
+
 // tag::website-links[]
 // end::website-links[]

Reply via email to