This is an automated email from the ASF dual-hosted git repository.
mattcasters 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 2a248a3a4e Show column definition in Database perspective, fixes #8464
(#8468)
2a248a3a4e is described below
commit 2a248a3a4eb92412f6e2e1b0d79582179b5cc7c5
Author: Matt Casters <[email protected]>
AuthorDate: Sun Sep 20 16:34:08 2026 +0200
Show column definition in Database perspective, fixes #8464 (#8468)
* Show column definition in Database perspective, fixes #8464
* Address review feedback on column definitions in Database perspective
(#8464)
- Drop unused sqlType and autoIncrement parameters from
DatabaseColumn.calculateDefinition
- Allow numeric/decimal precision up to 1000 when scale == 0 (e.g.
numeric(50))
- Remove redundant size < 10000000 check and duplicate DATE check
- Simplify empty typeName fallback in calculateDefinition(IValueMeta)
- Support sized bit(n) when n > 1 while preserving bare bit when n <= 1
- Validate TABLE_NAME, TABLE_CAT, and TABLE_SCHEM in readColumnDefinitions
to avoid cross-schema leakage
- Simplify fillColumns definition lookup to avoid redundant recalculation
- Add tests for bit(n), large numeric(50), wildcard character escaping in
table/schema, and schema filtering
---
.../hop/core/database/types/DatabaseColumn.java | 283 ++++++++++++++++++++-
.../database/types/StandardJdbcTypeMapper.java | 1 +
.../core/database/types/DatabaseColumnTest.java | 158 ++++++++++++
.../perspective/database/DatabaseTableInfoTab.java | 215 +++++++++++++++-
.../database/messages/messages_en_US.properties | 3 +-
.../database/DatabaseTableInfoTabTest.java | 186 ++++++++++++++
6 files changed, 834 insertions(+), 12 deletions(-)
diff --git
a/core/src/main/java/org/apache/hop/core/database/types/DatabaseColumn.java
b/core/src/main/java/org/apache/hop/core/database/types/DatabaseColumn.java
index 6a2d6fd4ba..46f0ed5a7a 100644
--- a/core/src/main/java/org/apache/hop/core/database/types/DatabaseColumn.java
+++ b/core/src/main/java/org/apache/hop/core/database/types/DatabaseColumn.java
@@ -19,6 +19,11 @@ package org.apache.hop.core.database.types;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
+import java.util.Locale;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.database.DatabaseMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.util.Utils;
/**
* A database column described independently of which JDBC metadata API it
came from.
@@ -41,6 +46,7 @@ public final class DatabaseColumn {
private final int displaySize;
private final boolean signed;
private final String comment;
+ private final boolean autoIncrement;
/**
* The result set metadata this column came from, or null when it came from
a getColumns() row.
@@ -61,6 +67,7 @@ public final class DatabaseColumn {
int displaySize,
boolean signed,
String comment,
+ boolean autoIncrement,
ResultSetMetaData resultSetMetaData,
int columnIndex) {
this.name = name;
@@ -72,6 +79,7 @@ public final class DatabaseColumn {
this.displaySize = displaySize;
this.signed = signed;
this.comment = comment;
+ this.autoIncrement = autoIncrement;
this.resultSetMetaData = resultSetMetaData;
this.columnIndex = columnIndex;
}
@@ -97,6 +105,7 @@ public final class DatabaseColumn {
rm.getColumnDisplaySize(index),
readSigned(rm, index),
rm.getColumnLabel(index),
+ readAutoIncrement(rm, index),
rm,
index);
}
@@ -113,6 +122,12 @@ public final class DatabaseColumn {
public static DatabaseColumn ofColumnsRow(ResultSet columnsRow) throws
SQLException {
int columnSize = columnsRow.getInt("COLUMN_SIZE");
Object decimalDigits = columnsRow.getObject("DECIMAL_DIGITS");
+ boolean autoIncrement = false;
+ try {
+ autoIncrement =
"YES".equalsIgnoreCase(columnsRow.getString("IS_AUTOINCREMENT"));
+ } catch (Exception ignored) {
+ // Driver-dependent.
+ }
return new DatabaseColumn(
columnsRow.getString("COLUMN_NAME"),
columnsRow.getString("TABLE_NAME"),
@@ -123,6 +138,7 @@ public final class DatabaseColumn {
columnSize,
true,
columnsRow.getString("REMARKS"),
+ autoIncrement,
null,
-1);
}
@@ -147,6 +163,19 @@ public final class DatabaseColumn {
}
}
+ /**
+ * Not every JDBC driver implements isAutoIncrement(); those that don't are
treated as not auto
+ * increment.
+ */
+ private static boolean readAutoIncrement(ResultSetMetaData rm, int index) {
+ try {
+ return rm.isAutoIncrement(index);
+ } catch (Exception ignored) {
+ // This JDBC driver doesn't support the isAutoIncrement method.
+ return false;
+ }
+ }
+
public String getName() {
return name;
}
@@ -194,6 +223,10 @@ public final class DatabaseColumn {
return comment;
}
+ public boolean isAutoIncrement() {
+ return autoIncrement;
+ }
+
public ResultSetMetaData getResultSetMetaData() {
return resultSetMetaData;
}
@@ -208,7 +241,255 @@ public final class DatabaseColumn {
*/
public static DatabaseColumn of(
String name, int sqlType, String nativeTypeName, int precision, int
scale, int displaySize) {
+ return of(name, sqlType, nativeTypeName, precision, scale, displaySize,
false);
+ }
+
+ /**
+ * Describes a column with an explicit auto increment indicator when the
caller already has JDBC
+ * type information.
+ */
+ public static DatabaseColumn of(
+ String name,
+ int sqlType,
+ String nativeTypeName,
+ int precision,
+ int scale,
+ int displaySize,
+ boolean autoIncrement) {
return new DatabaseColumn(
- name, null, sqlType, nativeTypeName, precision, scale, displaySize,
true, null, null, -1);
+ name,
+ null,
+ sqlType,
+ nativeTypeName,
+ precision,
+ scale,
+ displaySize,
+ true,
+ null,
+ autoIncrement,
+ null,
+ -1);
+ }
+
+ /**
+ * The definition of this column as defined in the database (for example:
varchar(100), bigint,
+ * int identity, bool, float, timestamp(6), numeric(10, 2)).
+ */
+ public String getDefinition() {
+ return calculateDefinition(
+ nativeTypeName, precision, scale, displaySize > 0 ? displaySize :
precision);
+ }
+
+ /**
+ * Calculates the database column definition for an {@link IValueMeta} using
whatever original
+ * database metadata was attached to it.
+ */
+ public static String calculateDefinition(IValueMeta valueMeta) {
+ if (valueMeta == null) {
+ return "";
+ }
+ String typeName = valueMeta.getOriginalColumnTypeName();
+ if (Utils.isEmpty(typeName)) {
+ return Const.NVL(valueMeta.getTypeDesc(), "");
+ }
+ int precision =
+ valueMeta.getLength() > 0 ? valueMeta.getLength() :
valueMeta.getOriginalPrecision();
+ int scale;
+ if (valueMeta.getType() == IValueMeta.TYPE_TIMESTAMP
+ || valueMeta.getType() == IValueMeta.TYPE_DATE) {
+ scale =
+ valueMeta.getOriginalScale() > 0 ? valueMeta.getOriginalScale() :
valueMeta.getLength();
+ } else {
+ scale =
+ valueMeta.getPrecision() >= 0 ? valueMeta.getPrecision() :
valueMeta.getOriginalScale();
+ }
+ int length = valueMeta.getLength();
+ return calculateDefinition(typeName, precision, scale, length);
+ }
+
+ /**
+ * Formats the database column data type definition given the native type
name and column
+ * dimensions.
+ */
+ public static String calculateDefinition(
+ String nativeTypeName, int precision, int scale, int length) {
+ if (Utils.isEmpty(nativeTypeName)) {
+ return "";
+ }
+ String trimmed = nativeTypeName.trim();
+ if (trimmed.contains("(")) {
+ return trimmed;
+ }
+
+ String upper = trimmed.toUpperCase(Locale.ROOT);
+
+ // 1. Types that do not take length/precision
+ if (isIntegerType(upper)
+ || isFloatingPointType(upper)
+ || isBooleanType(upper)
+ || isUnsizedType(upper)) {
+ return trimmed;
+ }
+
+ // 2. String/Character, Binary, and Bit types: VARCHAR, CHAR, NVARCHAR,
NCHAR, VARCHAR2,
+ // NVARCHAR2,
+ // BPCHAR, BINARY, VARBINARY, RAW, BIT
+ if (isSizedStringType(upper) || isSizedBinaryType(upper) ||
isSizedBitType(upper)) {
+ int size = precision > 0 ? precision : length;
+ if (upper.equals("BIT") && size <= 1) {
+ return trimmed;
+ }
+ if (size > 0 && size < DatabaseMeta.CLOB_LENGTH) {
+ return trimmed + "(" + size + ")";
+ }
+ return trimmed;
+ }
+
+ // 3. Decimal/Numeric types: DECIMAL, NUMERIC, NUMBER, DEC
+ if (isDecimalType(upper)) {
+ int p = precision > 0 ? precision : length;
+ if (p > 0 && p <= 1000) {
+ if (scale > 0) {
+ return trimmed + "(" + p + ", " + scale + ")";
+ } else if (scale == 0) {
+ return trimmed + "(" + p + ")";
+ }
+ }
+ return trimmed;
+ }
+
+ // 4. Timestamp / Time / DateTime types: TIMESTAMP, TIMESTAMPTZ, TIME,
TIMETZ, DATETIME2
+ if (isDateTimeWithPrecision(upper)) {
+ int fracSec =
+ (scale >= 0 && scale <= 9) ? scale : ((length >= 0 && length <= 9) ?
length : -1);
+ if (fracSec > 0) {
+ int tzIndex = -1;
+ if (upper.contains(" WITHOUT TIME ZONE")) {
+ tzIndex = upper.indexOf(" WITHOUT TIME ZONE");
+ } else if (upper.contains(" WITH LOCAL TIME ZONE")) {
+ tzIndex = upper.indexOf(" WITH LOCAL TIME ZONE");
+ } else if (upper.contains(" WITH TIME ZONE")) {
+ tzIndex = upper.indexOf(" WITH TIME ZONE");
+ }
+ if (tzIndex >= 0) {
+ return trimmed.substring(0, tzIndex) + "(" + fracSec + ")" +
trimmed.substring(tzIndex);
+ } else {
+ return trimmed + "(" + fracSec + ")";
+ }
+ }
+ return trimmed;
+ }
+
+ return trimmed;
+ }
+
+ private static boolean isIntegerType(String upper) {
+ return upper.equals("BIGINT")
+ || upper.startsWith("BIGINT ")
+ || upper.equals("INT")
+ || upper.startsWith("INT ")
+ || upper.equals("INTEGER")
+ || upper.startsWith("INTEGER ")
+ || upper.equals("SMALLINT")
+ || upper.startsWith("SMALLINT ")
+ || upper.equals("TINYINT")
+ || upper.startsWith("TINYINT ")
+ || upper.equals("MEDIUMINT")
+ || upper.startsWith("MEDIUMINT ")
+ || upper.equals("INT2")
+ || upper.equals("INT4")
+ || upper.equals("INT8")
+ || upper.equals("INT16")
+ || upper.equals("INT32")
+ || upper.equals("INT64")
+ || upper.equals("UINT")
+ || upper.equals("SERIAL")
+ || upper.equals("BIGSERIAL")
+ || upper.equals("SMALLSERIAL");
+ }
+
+ private static boolean isFloatingPointType(String upper) {
+ return upper.equals("FLOAT")
+ || upper.startsWith("FLOAT ")
+ || upper.equals("DOUBLE")
+ || upper.startsWith("DOUBLE ")
+ || upper.equals("REAL")
+ || upper.equals("DOUBLE PRECISION")
+ || upper.equals("FLOAT4")
+ || upper.equals("FLOAT8")
+ || upper.equals("BINARY_FLOAT")
+ || upper.equals("BINARY_DOUBLE");
+ }
+
+ private static boolean isBooleanType(String upper) {
+ return upper.equals("BOOL") || upper.equals("BOOLEAN");
+ }
+
+ private static boolean isSizedBitType(String upper) {
+ return upper.equals("BIT") || upper.equals("BIT VARYING") ||
upper.equals("VARBIT");
+ }
+
+ private static boolean isUnsizedType(String upper) {
+ return upper.equals("TEXT")
+ || upper.equals("TINYTEXT")
+ || upper.equals("MEDIUMTEXT")
+ || upper.equals("LONGTEXT")
+ || upper.equals("CLOB")
+ || upper.equals("NCLOB")
+ || upper.equals("BLOB")
+ || upper.equals("BYTEA")
+ || upper.equals("IMAGE")
+ || upper.equals("JSON")
+ || upper.equals("JSONB")
+ || upper.equals("XML")
+ || upper.equals("UUID")
+ || upper.equals("UNIQUEIDENTIFIER")
+ || upper.equals("MONEY")
+ || upper.equals("SMALLMONEY")
+ || upper.equals("DATETIME")
+ || upper.equals("SMALLDATETIME")
+ || upper.equals("ROWVERSION")
+ || upper.equals("DATE")
+ || upper.equals("GEOMETRY")
+ || upper.equals("GEOGRAPHY")
+ || upper.equals("HIERARCHYID")
+ || upper.equals("SQL_VARIANT")
+ || upper.equals("SYSNAME");
+ }
+
+ private static boolean isSizedStringType(String upper) {
+ return upper.equals("VARCHAR")
+ || upper.equals("CHAR")
+ || upper.equals("CHARACTER")
+ || upper.equals("CHARACTER VARYING")
+ || upper.equals("VARCHAR2")
+ || upper.equals("NVARCHAR")
+ || upper.equals("NVARCHAR2")
+ || upper.equals("NCHAR")
+ || upper.equals("BPCHAR");
+ }
+
+ private static boolean isSizedBinaryType(String upper) {
+ return upper.equals("BINARY") || upper.equals("VARBINARY") ||
upper.equals("RAW");
+ }
+
+ private static boolean isDecimalType(String upper) {
+ return upper.equals("DECIMAL")
+ || upper.equals("NUMERIC")
+ || upper.equals("NUMBER")
+ || upper.equals("DEC");
+ }
+
+ private static boolean isDateTimeWithPrecision(String upper) {
+ return upper.equals("TIMESTAMP")
+ || upper.startsWith("TIMESTAMP ")
+ || upper.equals("TIMESTAMPTZ")
+ || upper.startsWith("TIMESTAMPTZ ")
+ || upper.equals("DATETIME2")
+ || upper.startsWith("DATETIME2 ")
+ || upper.equals("TIME")
+ || upper.startsWith("TIME ")
+ || upper.equals("TIMETZ")
+ || upper.startsWith("TIMETZ ");
}
}
diff --git
a/core/src/main/java/org/apache/hop/core/database/types/StandardJdbcTypeMapper.java
b/core/src/main/java/org/apache/hop/core/database/types/StandardJdbcTypeMapper.java
index 751c809cbc..c2d8f2906c 100644
---
a/core/src/main/java/org/apache/hop/core/database/types/StandardJdbcTypeMapper.java
+++
b/core/src/main/java/org/apache/hop/core/database/types/StandardJdbcTypeMapper.java
@@ -333,6 +333,7 @@ public final class StandardJdbcTypeMapper {
valueMeta.setOriginalColumnTypeName(column.getNativeTypeName());
valueMeta.setOriginalPrecision(ignoreLength ? -1 : column.getPrecision());
valueMeta.setOriginalScale(column.getScale());
+ valueMeta.setOriginalAutoIncrement(column.isAutoIncrement());
valueMeta.setOriginalSigned(column.isSigned());
}
}
diff --git
a/core/src/test/java/org/apache/hop/core/database/types/DatabaseColumnTest.java
b/core/src/test/java/org/apache/hop/core/database/types/DatabaseColumnTest.java
new file mode 100644
index 0000000000..2ea961178f
--- /dev/null
+++
b/core/src/test/java/org/apache/hop/core/database/types/DatabaseColumnTest.java
@@ -0,0 +1,158 @@
+/*
+ * 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.core.database.types;
+
+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 java.sql.Types;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+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.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+class DatabaseColumnTest {
+
+ @BeforeAll
+ static void setUp() throws Exception {
+ HopClientEnvironment.init();
+ }
+
+ @Test
+ void calculatesDefinitionsForStandardTypes() {
+ // Exact examples from issue #8464:
+ // varchar(100), bigint, int identity, bool, float, timestamp(6)
+ assertEquals("varchar(100)", DatabaseColumn.calculateDefinition("varchar",
100, 0, 100));
+ assertEquals("VARCHAR(50)", DatabaseColumn.calculateDefinition("VARCHAR",
50, 0, 50));
+ assertEquals("bigint", DatabaseColumn.calculateDefinition("bigint", 19, 0,
15));
+ assertEquals("int identity", DatabaseColumn.calculateDefinition("int
identity", 10, 0, 9));
+ assertEquals("bool", DatabaseColumn.calculateDefinition("bool", 1, 0, -1));
+ assertEquals("float", DatabaseColumn.calculateDefinition("float", 53, 0,
-1));
+ assertEquals("timestamp(6)",
DatabaseColumn.calculateDefinition("timestamp", 26, 6, 6));
+ }
+
+ @Test
+ void calculatesNumericAndDecimalDefinitions() {
+ assertEquals("numeric(10, 2)",
DatabaseColumn.calculateDefinition("numeric", 10, 2, 10));
+ assertEquals("DECIMAL(12, 4)",
DatabaseColumn.calculateDefinition("DECIMAL", 12, 4, 12));
+ assertEquals("NUMBER(10)", DatabaseColumn.calculateDefinition("NUMBER",
10, 0, 10));
+ assertEquals("numeric(50)", DatabaseColumn.calculateDefinition("numeric",
50, 0, 50));
+ assertEquals("numeric(50, 2)",
DatabaseColumn.calculateDefinition("numeric", 50, 2, 50));
+ assertEquals("numeric", DatabaseColumn.calculateDefinition("numeric", 0,
0, -1));
+ }
+
+ @Test
+ void calculatesDateTimeDefinitions() {
+ assertEquals("datetime2(7)",
DatabaseColumn.calculateDefinition("datetime2", 27, 7, 7));
+ assertEquals("time(3)", DatabaseColumn.calculateDefinition("time", 12, 3,
3));
+ assertEquals("timestamptz(6)",
DatabaseColumn.calculateDefinition("timestamptz", 29, 6, 6));
+ assertEquals(
+ "timestamp(6) without time zone",
+ DatabaseColumn.calculateDefinition("timestamp without time zone", 29,
6, 6));
+ assertEquals(
+ "timestamp(6) with local time zone",
+ DatabaseColumn.calculateDefinition("timestamp with local time zone",
29, 6, 6));
+ assertEquals(
+ "TIMESTAMP(6) WITH LOCAL TIME ZONE",
+ DatabaseColumn.calculateDefinition("TIMESTAMP WITH LOCAL TIME ZONE",
29, 6, 6));
+ assertEquals("timestamp", DatabaseColumn.calculateDefinition("timestamp",
19, 0, 0));
+ assertEquals("date", DatabaseColumn.calculateDefinition("date", 10, 0,
-1));
+ assertEquals("datetime", DatabaseColumn.calculateDefinition("datetime",
23, 3, 3));
+ assertEquals("smalldatetime",
DatabaseColumn.calculateDefinition("smalldatetime", 16, 0, 0));
+ }
+
+ @Test
+ void calculatesSizedCharacterAndBinaryDefinitions() {
+ assertEquals("char(10)", DatabaseColumn.calculateDefinition("char", 10, 0,
10));
+ assertEquals("nvarchar(50)",
DatabaseColumn.calculateDefinition("nvarchar", 50, 0, 50));
+ assertEquals("varbinary(64)",
DatabaseColumn.calculateDefinition("varbinary", 64, 0, 64));
+ assertEquals("binary(16)", DatabaseColumn.calculateDefinition("binary",
16, 0, 16));
+ assertEquals("bit", DatabaseColumn.calculateDefinition("bit", 1, 0, 1));
+ assertEquals("bit", DatabaseColumn.calculateDefinition("bit", 0, 0, -1));
+ assertEquals("bit(8)", DatabaseColumn.calculateDefinition("bit", 8, 0, 8));
+ assertEquals("varbit(16)", DatabaseColumn.calculateDefinition("varbit",
16, 0, 16));
+ }
+
+ @Test
+ void preservesUnsizedAndComplexDefinitions() {
+ assertEquals("text", DatabaseColumn.calculateDefinition("text",
2147483647, 0, -1));
+ assertEquals("clob", DatabaseColumn.calculateDefinition("clob",
2147483647, 0, -1));
+ assertEquals("blob", DatabaseColumn.calculateDefinition("blob",
2147483647, 0, -1));
+ assertEquals("json", DatabaseColumn.calculateDefinition("json", 0, 0, -1));
+ assertEquals("jsonb", DatabaseColumn.calculateDefinition("jsonb", 0, 0,
-1));
+ assertEquals("uuid", DatabaseColumn.calculateDefinition("uuid", 0, 0, -1));
+ assertEquals(
+ "uniqueidentifier",
DatabaseColumn.calculateDefinition("uniqueidentifier", 36, 0, 36));
+ assertEquals("money", DatabaseColumn.calculateDefinition("money", 19, 4,
19));
+ assertEquals("smallmoney",
DatabaseColumn.calculateDefinition("smallmoney", 10, 4, 10));
+ assertEquals("varchar(100)",
DatabaseColumn.calculateDefinition("varchar(100)", 100, 0, 100));
+ assertEquals("enum('a', 'b')",
DatabaseColumn.calculateDefinition("enum('a', 'b')", 0, 0, -1));
+ }
+
+ @Test
+ void calculatesDefinitionFromDatabaseColumnInstance() {
+ DatabaseColumn col = DatabaseColumn.of("c_name", Types.VARCHAR, "varchar",
100, 0, 100);
+ assertEquals("varchar(100)", col.getDefinition());
+ assertFalse(col.isAutoIncrement());
+
+ DatabaseColumn colAi = DatabaseColumn.of("id", Types.INTEGER, "int", 10,
0, 9, true);
+ assertEquals("int", colAi.getDefinition());
+ assertTrue(colAi.isAutoIncrement());
+
+ DatabaseColumn colIdentity =
+ DatabaseColumn.of("id", Types.INTEGER, "int identity", 10, 0, 9, true);
+ assertEquals("int identity", colIdentity.getDefinition());
+ }
+
+ @Test
+ void calculatesDefinitionFromValueMeta() {
+ IValueMeta vmString = new ValueMetaString("name");
+ vmString.setOriginalColumnTypeName("varchar");
+ vmString.setOriginalColumnType(Types.VARCHAR);
+ vmString.setLength(100);
+ assertEquals("varchar(100)", DatabaseColumn.calculateDefinition(vmString));
+
+ IValueMeta vmInt = new ValueMetaInteger("id");
+ vmInt.setOriginalColumnTypeName("bigint");
+ vmInt.setOriginalColumnType(Types.BIGINT);
+ vmInt.setLength(15);
+ assertEquals("bigint", DatabaseColumn.calculateDefinition(vmInt));
+
+ IValueMeta vmNumeric = new ValueMetaNumber("amount");
+ vmNumeric.setOriginalColumnTypeName("numeric");
+ vmNumeric.setOriginalColumnType(Types.NUMERIC);
+ vmNumeric.setLength(10);
+ vmNumeric.setPrecision(2);
+ assertEquals("numeric(10, 2)",
DatabaseColumn.calculateDefinition(vmNumeric));
+
+ IValueMeta vmTs = new ValueMetaTimestamp("ts");
+ vmTs.setOriginalColumnTypeName("timestamp");
+ vmTs.setOriginalColumnType(Types.TIMESTAMP);
+ vmTs.setLength(6);
+ assertEquals("timestamp(6)", DatabaseColumn.calculateDefinition(vmTs));
+
+ // Fallback to Hop type description when originalColumnTypeName is null
+ IValueMeta vmFallback = new ValueMetaString("raw_field");
+ assertEquals("String", DatabaseColumn.calculateDefinition(vmFallback));
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
index 3d4365a0ad..2397c0217e 100644
---
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTab.java
@@ -32,6 +32,7 @@ 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.database.DatabaseObjectDdl;
+import org.apache.hop.core.database.types.DatabaseColumn;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.row.IRowMeta;
import org.apache.hop.core.row.IValueMeta;
@@ -205,6 +206,7 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
IRowMeta fields;
List<DatabaseIndexInfo> indexes;
String ddl;
+ Map<String, String> definitions;
boolean view = kind == DatabaseTreeNode.Kind.VIEW;
try (Database db =
new Database(host.getLoggingObject(), host.getVariables(),
databaseMeta)) {
@@ -215,6 +217,7 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
}
fields = loadFields(db, qualified);
indexes = loadIndexes(db, schemaName, tableName);
+ definitions = loadColumnDefinitions(db, schemaName, tableName,
fields);
try {
ddl = db.getObjectDdl(schemaName, tableName, view, fields);
} catch (Exception e) {
@@ -223,12 +226,13 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
}
IRowMeta loadedFields = fields;
List<DatabaseIndexInfo> loadedIndexes = indexes;
+ Map<String, String> loadedDefinitions = definitions;
String loadedDdl =
view
? ddl
: withIndexStatements(
databaseMeta, host.getVariables(), schemaName,
tableName, ddl, indexes);
- host.asyncExec(() -> populate(loadedFields, loadedIndexes,
loadedDdl));
+ host.asyncExec(() -> populate(loadedFields, loadedIndexes,
loadedDdl, loadedDefinitions));
});
}
@@ -277,11 +281,193 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
return new ArrayList<>(byName.values());
}
- private void populate(IRowMeta fields, List<DatabaseIndexInfo> indexes,
String ddl) {
+ static Map<String, String> loadColumnDefinitions(
+ Database db, String schemaName, String tableName, IRowMeta fields) {
+ Map<String, String> definitions = new HashMap<>();
+ if (db != null) {
+ try {
+ DatabaseMetaData metaData = db.getDatabaseMetaData();
+ if (metaData != null) {
+ String escape = null;
+ try {
+ escape = metaData.getSearchStringEscape();
+ } catch (Exception ignored) {
+ // Driver-dependent.
+ }
+ boolean supportsCatalogs = false;
+ boolean supportsSchemas = false;
+ try {
+ supportsCatalogs = metaData.supportsCatalogsInTableDefinitions();
+ supportsSchemas = metaData.supportsSchemasInTableDefinitions();
+ } catch (Exception ignored) {
+ // Driver-dependent.
+ }
+
+ String catalog = null;
+ String schemaPattern = Utils.isEmpty(schemaName) ? null : schemaName;
+ if (supportsCatalogs && !supportsSchemas) {
+ // Catalogs are used instead of schemas (e.g. MySQL / MariaDB)
+ catalog = schemaName;
+ schemaPattern = null;
+ } else {
+ try {
+ if (supportsCatalogs && db.getConnection() != null) {
+ catalog = db.getConnection().getCatalog();
+ }
+ } catch (Exception ignored) {
+ // Driver-dependent.
+ }
+ }
+
+ String escapedSchema = escapePattern(schemaPattern, escape);
+ String escapedTable = escapePattern(tableName, escape);
+
+ readColumnDefinitions(
+ metaData,
+ catalog,
+ escapedSchema,
+ escapedTable,
+ catalog,
+ schemaPattern,
+ tableName,
+ definitions);
+
+ // If empty and schemaName was provided, try using schemaName as
catalog
+ if (definitions.isEmpty() && !Utils.isEmpty(schemaName)) {
+ if (catalog == null || !schemaName.equals(catalog)) {
+ readColumnDefinitions(
+ metaData,
+ schemaName,
+ null,
+ escapedTable,
+ schemaName,
+ null,
+ tableName,
+ definitions);
+ }
+ }
+
+ if (definitions.isEmpty() && !Utils.isEmpty(tableName)) {
+ String upperTable =
escapePattern(tableName.toUpperCase(Locale.ROOT), escape);
+ readColumnDefinitions(
+ metaData,
+ catalog,
+ escapedSchema,
+ upperTable,
+ catalog,
+ schemaPattern,
+ tableName,
+ definitions);
+ if (definitions.isEmpty()) {
+ String lowerTable =
escapePattern(tableName.toLowerCase(Locale.ROOT), escape);
+ readColumnDefinitions(
+ metaData,
+ catalog,
+ escapedSchema,
+ lowerTable,
+ catalog,
+ schemaPattern,
+ tableName,
+ definitions);
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // Fall back to fields.
+ }
+ }
+ if (fields != null) {
+ for (int i = 0; i < fields.size(); i++) {
+ IValueMeta value = fields.getValueMeta(i);
+ if (value != null && !Utils.isEmpty(value.getName())) {
+ String key = value.getName().toLowerCase(Locale.ROOT);
+ if (!definitions.containsKey(key)) {
+ definitions.put(key, DatabaseColumn.calculateDefinition(value));
+ }
+ }
+ }
+ }
+ return definitions;
+ }
+
+ static String escapePattern(String value, String escape) {
+ if (Utils.isEmpty(value) || Utils.isEmpty(escape)) {
+ return value;
+ }
+ char escapeChar = escape.charAt(0);
+ StringBuilder escaped = new StringBuilder(value.length() + 4);
+ for (char c : value.toCharArray()) {
+ if (c == '_' || c == '%' || c == escapeChar) {
+ escaped.append(escapeChar);
+ }
+ escaped.append(c);
+ }
+ return escaped.toString();
+ }
+
+ private static void readColumnDefinitions(
+ DatabaseMetaData metaData,
+ String catalog,
+ String schemaPattern,
+ String tableNamePattern,
+ String expectedCatalog,
+ String expectedSchema,
+ String expectedTableName,
+ Map<String, String> definitions) {
+ try (ResultSet columns = metaData.getColumns(catalog, schemaPattern,
tableNamePattern, null)) {
+ if (columns != null) {
+ String chosenSchema = null;
+ while (columns.next()) {
+ try {
+ if (!Utils.isEmpty(expectedTableName)) {
+ String actualTable = columns.getString("TABLE_NAME");
+ if (!Utils.isEmpty(actualTable) &&
!expectedTableName.equalsIgnoreCase(actualTable)) {
+ continue;
+ }
+ }
+ String actualSchema = columns.getString("TABLE_SCHEM");
+ if (!Utils.isEmpty(expectedSchema)) {
+ if (!Utils.isEmpty(actualSchema) &&
!expectedSchema.equalsIgnoreCase(actualSchema)) {
+ continue;
+ }
+ } else if (!Utils.isEmpty(actualSchema)) {
+ if (chosenSchema == null) {
+ chosenSchema = actualSchema;
+ } else if (!chosenSchema.equalsIgnoreCase(actualSchema)) {
+ continue;
+ }
+ }
+ if (!Utils.isEmpty(expectedCatalog)) {
+ String actualCatalog = columns.getString("TABLE_CAT");
+ if (!Utils.isEmpty(actualCatalog)
+ && !expectedCatalog.equalsIgnoreCase(actualCatalog)) {
+ continue;
+ }
+ }
+ DatabaseColumn column = DatabaseColumn.ofColumnsRow(columns);
+ if (column != null && !Utils.isEmpty(column.getName())) {
+ definitions.putIfAbsent(
+ column.getName().toLowerCase(Locale.ROOT),
column.getDefinition());
+ }
+ } catch (Exception ignored) {
+ // Ignore single column read errors
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // Driver error on getColumns
+ }
+ }
+
+ private void populate(
+ IRowMeta fields,
+ List<DatabaseIndexInfo> indexes,
+ String ddl,
+ Map<String, String> definitions) {
if (control.isDisposed()) {
return;
}
- fillColumns(fields);
+ fillColumns(fields, definitions);
fillIndexes(indexes);
fillDdl(ddl);
}
@@ -346,7 +532,7 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
|| DatabaseObjectDdl.startsWithCreate(ddl) && upper.contains(" KEY ");
}
- private void fillColumns(IRowMeta fields) {
+ private void fillColumns(IRowMeta fields, Map<String, String> definitions) {
columnsView.clearAll(false);
if (fields == null) {
columnsView.removeEmptyRows();
@@ -357,11 +543,16 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
IValueMeta value = fields.getValueMeta(i);
TableItem item =
i == 0 ? columnsView.table.getItem(0) : new
TableItem(columnsView.table, SWT.NONE);
+ String definition =
+ definitions != null && !Utils.isEmpty(value.getName())
+ ?
Const.NVL(definitions.get(value.getName().toLowerCase(Locale.ROOT)), "")
+ : DatabaseColumn.calculateDefinition(value);
item.setText(1, Const.NVL(value.getName(), ""));
- item.setText(2, Const.NVL(value.getTypeDesc(), ""));
- item.setText(3, value.getLength() >= 0 ?
Integer.toString(value.getLength()) : "");
- item.setText(4, value.getPrecision() >= 0 ?
Integer.toString(value.getPrecision()) : "");
- item.setText(5, Const.NVL(value.getComments(), ""));
+ item.setText(2, Const.NVL(definition, ""));
+ item.setText(3, Const.NVL(value.getTypeDesc(), ""));
+ item.setText(4, value.getLength() >= 0 ?
Integer.toString(value.getLength()) : "");
+ item.setText(5, value.getPrecision() >= 0 ?
Integer.toString(value.getPrecision()) : "");
+ item.setText(6, Const.NVL(value.getComments(), ""));
}
columnsView.removeEmptyRows();
columnsView.setRowNums();
@@ -392,14 +583,18 @@ public class DatabaseTableInfoTab implements
IHopFileTypeHandler {
indexesView.optWidth(true);
}
- private ColumnInfo[] columnInfos() {
+ static ColumnInfo[] columnInfos() {
return new ColumnInfo[] {
new ColumnInfo(
BaseMessages.getString(PKG,
"DatabasePerspective.TableInfo.Column.Name"),
ColumnInfo.COLUMN_TYPE_TEXT,
false),
new ColumnInfo(
- BaseMessages.getString(PKG,
"DatabasePerspective.TableInfo.Column.Type"),
+ BaseMessages.getString(PKG,
"DatabasePerspective.TableInfo.Column.Definition"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false),
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"DatabasePerspective.TableInfo.Column.HopType"),
ColumnInfo.COLUMN_TYPE_TEXT,
false),
new ColumnInfo(
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 21ccda47e7..80688d969c 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
@@ -87,7 +87,8 @@ DatabasePerspective.TableInfo.Columns=Columns
DatabasePerspective.TableInfo.Indexes=Indexes
DatabasePerspective.TableInfo.Ddl=DDL
DatabasePerspective.TableInfo.Column.Name=Name
-DatabasePerspective.TableInfo.Column.Type=Type
+DatabasePerspective.TableInfo.Column.Definition=Definition
+DatabasePerspective.TableInfo.Column.HopType=Hop Type
DatabasePerspective.TableInfo.Column.Length=Length
DatabasePerspective.TableInfo.Column.Precision=Precision
DatabasePerspective.TableInfo.Column.Comments=Comments
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTabTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTabTest.java
index ee7614813f..59f3f607c3 100644
---
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTabTest.java
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/database/DatabaseTableInfoTabTest.java
@@ -25,9 +25,20 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hop.core.database.Database;
import org.apache.hop.core.database.DatabaseMeta;
+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.ValueMetaString;
import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.ui.core.widget.ColumnInfo;
import org.junit.jupiter.api.Test;
class DatabaseTableInfoTabTest {
@@ -78,4 +89,179 @@ class DatabaseTableInfoTabTest {
"CREATE TABLE `t` (\n `id` int,\n KEY `idx` (`id`)\n)"));
assertFalse(DatabaseTableInfoTab.catalogDdlIncludesIndexes("CREATE TABLE t
(id INTEGER);"));
}
+
+ @Test
+ void columnInfosIncludesDefinition() {
+ ColumnInfo[] infos = DatabaseTableInfoTab.columnInfos();
+ assertEquals(6, infos.length);
+ assertEquals("Name", infos[0].getName());
+ assertEquals("Definition", infos[1].getName());
+ assertEquals("Hop Type", infos[2].getName());
+ assertEquals("Length", infos[3].getName());
+ assertEquals("Precision", infos[4].getName());
+ assertEquals("Comments", infos[5].getName());
+ }
+
+ @Test
+ void loadColumnDefinitionsFromMetaData() throws Exception {
+ Database db = mock(Database.class);
+ DatabaseMetaData metaData = mock(DatabaseMetaData.class);
+ ResultSet rs = mock(ResultSet.class);
+ ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
+
+ when(db.getDatabaseMetaData()).thenReturn(metaData);
+ when(metaData.getColumns(any(), any(), eq("orders"),
any())).thenReturn(rs);
+ when(rs.getMetaData()).thenReturn(rsmd);
+ when(rsmd.getColumnCount()).thenReturn(10);
+ when(rs.next()).thenReturn(true, false);
+ when(rs.getString("COLUMN_NAME")).thenReturn("customer_name");
+ when(rs.getInt("DATA_TYPE")).thenReturn(java.sql.Types.VARCHAR);
+ when(rs.getString("TYPE_NAME")).thenReturn("varchar");
+ when(rs.getInt("COLUMN_SIZE")).thenReturn(100);
+
+ Map<String, String> defs =
+ DatabaseTableInfoTab.loadColumnDefinitions(db, "public", "orders",
null);
+ assertEquals("varchar(100)", defs.get("customer_name"));
+ }
+
+ @Test
+ void loadColumnDefinitionsFallsBackToFields() {
+ IRowMeta rowMeta = new RowMeta();
+ IValueMeta vm = new ValueMetaString("notes");
+ vm.setLength(255);
+ vm.setOriginalColumnTypeName("varchar");
+ rowMeta.addValueMeta(vm);
+
+ Map<String, String> defs =
+ DatabaseTableInfoTab.loadColumnDefinitions(null, "public", "orders",
rowMeta);
+ assertEquals("varchar(255)", defs.get("notes"));
+ }
+
+ @Test
+ void escapePatternEscapesSpecialCharacters() {
+ assertEquals("order\\_items",
DatabaseTableInfoTab.escapePattern("order_items", "\\"));
+ assertEquals("sales\\%total",
DatabaseTableInfoTab.escapePattern("sales%total", "\\"));
+ assertEquals("path\\\\test",
DatabaseTableInfoTab.escapePattern("path\\test", "\\"));
+ assertEquals("plain_text",
DatabaseTableInfoTab.escapePattern("plain_text", null));
+ assertEquals("plain_text",
DatabaseTableInfoTab.escapePattern("plain_text", ""));
+ }
+
+ @Test
+ void loadColumnDefinitionsUsesCatalogWhenSchemasNotSupported() throws
Exception {
+ Database db = mock(Database.class);
+ DatabaseMetaData metaData = mock(DatabaseMetaData.class);
+ ResultSet rs = mock(ResultSet.class);
+ ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
+
+ when(db.getDatabaseMetaData()).thenReturn(metaData);
+ when(metaData.supportsCatalogsInTableDefinitions()).thenReturn(true);
+ when(metaData.supportsSchemasInTableDefinitions()).thenReturn(false);
+ when(metaData.getSearchStringEscape()).thenReturn("\\");
+ when(metaData.getColumns(eq("otherdb"), eq(null), eq("orders"),
any())).thenReturn(rs);
+ when(rs.getMetaData()).thenReturn(rsmd);
+ when(rsmd.getColumnCount()).thenReturn(10);
+ when(rs.next()).thenReturn(true, false);
+ when(rs.getString("COLUMN_NAME")).thenReturn("id");
+ when(rs.getInt("DATA_TYPE")).thenReturn(java.sql.Types.INTEGER);
+ when(rs.getString("TYPE_NAME")).thenReturn("int");
+
+ Map<String, String> defs =
+ DatabaseTableInfoTab.loadColumnDefinitions(db, "otherdb", "orders",
null);
+ assertEquals("int", defs.get("id"));
+ }
+
+ @Test
+ void loadColumnDefinitionsEscapesWildcardCharactersInTableNameAndSchema()
throws Exception {
+ Database db = mock(Database.class);
+ DatabaseMetaData metaData = mock(DatabaseMetaData.class);
+ ResultSet rs = mock(ResultSet.class);
+ ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
+
+ when(db.getDatabaseMetaData()).thenReturn(metaData);
+ when(metaData.supportsCatalogsInTableDefinitions()).thenReturn(false);
+ when(metaData.supportsSchemasInTableDefinitions()).thenReturn(true);
+ when(metaData.getSearchStringEscape()).thenReturn("\\");
+ when(metaData.getColumns(any(), eq("sales\\_schema"),
eq("order\\_items\\%2024"), any()))
+ .thenReturn(rs);
+ when(rs.getMetaData()).thenReturn(rsmd);
+ when(rsmd.getColumnCount()).thenReturn(10);
+ when(rs.next()).thenReturn(true, false);
+ when(rs.getString("TABLE_NAME")).thenReturn("order_items%2024");
+ when(rs.getString("TABLE_SCHEM")).thenReturn("sales_schema");
+ when(rs.getString("COLUMN_NAME")).thenReturn("item_id");
+ when(rs.getInt("DATA_TYPE")).thenReturn(java.sql.Types.INTEGER);
+ when(rs.getString("TYPE_NAME")).thenReturn("int");
+
+ Map<String, String> defs =
+ DatabaseTableInfoTab.loadColumnDefinitions(db, "sales_schema",
"order_items%2024", null);
+ assertEquals("int", defs.get("item_id"));
+ }
+
+ @Test
+ void loadColumnDefinitionsFiltersMismatchedTablesAndSchemas() throws
Exception {
+ Database db = mock(Database.class);
+ DatabaseMetaData metaData = mock(DatabaseMetaData.class);
+ ResultSet rs = mock(ResultSet.class);
+ ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
+
+ when(db.getDatabaseMetaData()).thenReturn(metaData);
+ when(metaData.supportsCatalogsInTableDefinitions()).thenReturn(false);
+ when(metaData.supportsSchemasInTableDefinitions()).thenReturn(true);
+ when(metaData.getSearchStringEscape()).thenReturn("\\");
+ when(metaData.getColumns(any(), eq("sales"), eq("orders"),
any())).thenReturn(rs);
+ when(rs.getMetaData()).thenReturn(rsmd);
+ when(rsmd.getColumnCount()).thenReturn(10);
+ AtomicInteger row = new AtomicInteger(0);
+ when(rs.next()).thenAnswer(inv -> row.incrementAndGet() <= 3);
+ when(rs.getString("TABLE_NAME"))
+ .thenAnswer(
+ inv -> {
+ switch (row.get()) {
+ case 1:
+ return "orders_extra";
+ case 2:
+ case 3:
+ return "orders";
+ default:
+ return null;
+ }
+ });
+ when(rs.getString("TABLE_SCHEM"))
+ .thenAnswer(
+ inv -> {
+ switch (row.get()) {
+ case 1:
+ case 3:
+ return "sales";
+ case 2:
+ return "other_schema";
+ default:
+ return null;
+ }
+ });
+ when(rs.getString("COLUMN_NAME"))
+ .thenAnswer(
+ inv -> {
+ switch (row.get()) {
+ case 1:
+ return "extra_col";
+ case 2:
+ return "wrong_col";
+ case 3:
+ return "valid_col";
+ default:
+ return null;
+ }
+ });
+ when(rs.getInt("DATA_TYPE")).thenReturn(java.sql.Types.VARCHAR);
+ when(rs.getString("TYPE_NAME")).thenReturn("varchar");
+ when(rs.getInt("COLUMN_SIZE")).thenReturn(50);
+
+ Map<String, String> defs =
+ DatabaseTableInfoTab.loadColumnDefinitions(db, "sales", "orders",
null);
+ assertEquals(1, defs.size());
+ assertEquals("varchar(50)", defs.get("valid_col"));
+ assertFalse(defs.containsKey("extra_col"));
+ assertFalse(defs.containsKey("wrong_col"));
+ }
}