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 f6e8a6d424 Enhance DatabaseJoin with SQL parameter support (#8339)
f6e8a6d424 is described below
commit f6e8a6d4247a15514d840a215ad5b0c0a6562504
Author: Happy <[email protected]>
AuthorDate: Thu Sep 17 15:42:49 2026 +0800
Enhance DatabaseJoin with SQL parameter support (#8339)
* Enhance DatabaseJoin to support named and positional SQL parameters
* Enhance DatabaseJoin to support runtime output metadata initialization
and improve parameter handling
* Add tests for mixed named and positional SQL parameters in
DatabaseJoinMeta
* Added a new core SQL parser: parseSqlParameterSpec(String) that returns
* Refactor DatabaseJoin to simplify output field name handling and ensure
parameter specification is initialized
* Refactor DatabaseJoinMeta to streamline SQL parameter handling and
enhance metadata discovery
* Add tests for SQL parameter parsing and metadata handling in
DatabaseJoinMeta
* Fix SQL parameter parsing for JSONB operators and ARRAY expressions
- Skip only explicit JSONB operator tokens: `?|`, `?&`, `??`, `??|`,
and `??&`.
- Consume complete operator tokens so `col ?? ?` still counts the trailing
`?` as a bind parameter.
- Stop treating all `[...]` expressions as bracket-quoted identifiers.
- Add dialect-aware bracket handling for SQL Server.
- Preserve PostgreSQL parameter markers in `ARRAY[?]` and `ARRAY[?, ?]`.
- Apply the same parsing behavior to the Database Join path.
- Expand regression coverage for JSONB operators, `LIKE`, `NOT LIKE`,
`BETWEEN`, `SELECT ? FROM dual`, and ARRAY expressions.
* Enhance parseSqlParameterSpec() call in DatabaseJoin to support
bracket-quoted identifiers
* Refactor parseSqlParameterSpec() in DatabaseJoinMeta to support
bracket-quoted identifiers
* Add tests for mixed named and positional placeholders in SQL parameter
parsing
* Refactor SQL parameter tests to improve clarity and coverage for JSONB
operators and bracket-quoted identifiers
* Add resolved parameters panel to DatabaseJoinDialog for improved SQL
parameter handling
---------
Co-authored-by: happy.he <[email protected]>
---
.../org/apache/hop/core/database/Database.java | 262 ++++++++++++--
.../org/apache/hop/core/database/DatabaseTest.java | 66 ++++
.../transforms/databasejoin/DatabaseJoin.java | 71 +++-
.../transforms/databasejoin/DatabaseJoinData.java | 6 +
.../databasejoin/DatabaseJoinDialog.java | 205 ++++++++++-
.../transforms/databasejoin/DatabaseJoinMeta.java | 392 +++++++++++++++++----
.../messages/messages_en_US.properties | 6 +
.../databasejoin/DatabaseJoinMetaTest.java | 163 +++++++--
8 files changed, 1046 insertions(+), 125 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 bc44cf0e33..c8cd1fe566 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
@@ -3582,32 +3582,254 @@ public class Database implements IVariables,
ILoggingObject, AutoCloseable {
return (sqlType == Types.NUMERIC || sqlType == Types.DECIMAL) && precision
<= 0 && scale <= 0;
}
- public int countParameters(String sql) {
- int q = 0;
- boolean quoteOpened = false;
- boolean dquoteOpened = false;
+ /** Parsed SQL parameter information: prepared SQL plus ordered parameter
references. */
+ public static final class SqlParameterSpec {
+ private final String preparedSql;
+ private final List<String> parameterReferences;
- for (int x = 0; x < sql.length(); x++) {
- char c = sql.charAt(x);
+ public SqlParameterSpec(String preparedSql, List<String>
parameterReferences) {
+ this.preparedSql = preparedSql;
+ this.parameterReferences = Collections.unmodifiableList(new
ArrayList<>(parameterReferences));
+ }
- switch (c) {
- case '\'':
- quoteOpened = !quoteOpened;
- break;
- case '"':
- dquoteOpened = !dquoteOpened;
- break;
- case '?':
- if (!quoteOpened && !dquoteOpened) {
- q++;
+ public String getPreparedSql() {
+ return preparedSql;
+ }
+
+ /** Ordered references: named parameter field name or null for positional
'?'. */
+ public List<String> getParameterReferences() {
+ return parameterReferences;
+ }
+
+ public int getParameterCount() {
+ return parameterReferences.size();
+ }
+ }
+
+ /**
+ * Parse SQL for JDBC parameter markers and ?{name} placeholders.
+ *
+ * <p>The parser skips SQL text within literals, quoted identifiers and
comments. PostgreSQL JSONB
+ * operators (?|, ?&, ??, ??|, ??&) are not treated as parameters.
+ */
+ public static SqlParameterSpec parseSqlParameterSpec(String sourceSql) {
+ return parseSqlParameterSpec(sourceSql, false);
+ }
+
+ /**
+ * Parse SQL for JDBC parameter markers and ?{name} placeholders.
+ *
+ * <p>The parser skips SQL text within literals, quoted identifiers and
comments. PostgreSQL JSONB
+ * operators (?|, ?&, ??, ??|, ??&) are not treated as parameters.
+ *
+ * @param sourceSql the SQL text to parse
+ * @param consumeBracketIdentifiers true to treat [identifier] sections as
quoted identifiers
+ */
+ public static SqlParameterSpec parseSqlParameterSpec(
+ String sourceSql, boolean consumeBracketIdentifiers) {
+ String sql = Const.NVL(sourceSql, "");
+ StringBuilder preparedSql = new StringBuilder(sql.length());
+ List<String> parameterReferences = new ArrayList<>();
+
+ int index = 0;
+ while (index < sql.length()) {
+ char c = sql.charAt(index);
+
+ if (c == '-' && index + 1 < sql.length() && sql.charAt(index + 1) ==
'-') {
+ int end = index + 2;
+ while (end < sql.length()) {
+ char current = sql.charAt(end);
+ if (current == '\n' || current == '\r') {
+ break;
}
- break;
- default:
- break;
+ end++;
+ }
+ preparedSql.append(sql, index, end);
+ index = end;
+ continue;
+ }
+
+ if (c == '/' && index + 1 < sql.length() && sql.charAt(index + 1) ==
'*') {
+ int end = index + 2;
+ boolean closed = false;
+ while (end + 1 < sql.length()) {
+ if (sql.charAt(end) == '*' && sql.charAt(end + 1) == '/') {
+ end += 2;
+ closed = true;
+ break;
+ }
+ end++;
+ }
+ if (!closed) {
+ end = sql.length();
+ }
+ preparedSql.append(sql, index, end);
+ index = end;
+ continue;
+ }
+
+ if (c == '\'') {
+ int end = consumeQuotedSection(sql, index, '\'', true);
+ preparedSql.append(sql, index, end);
+ index = end;
+ continue;
+ }
+
+ if (c == '"') {
+ int end = consumeQuotedSection(sql, index, '"', true);
+ preparedSql.append(sql, index, end);
+ index = end;
+ continue;
+ }
+
+ if (c == '`') {
+ int end = consumeQuotedSection(sql, index, '`', true);
+ preparedSql.append(sql, index, end);
+ index = end;
+ continue;
+ }
+
+ if (consumeBracketIdentifiers && c == '[') {
+ int end = consumeBracketIdentifier(sql, index);
+ preparedSql.append(sql, index, end);
+ index = end;
+ continue;
+ }
+
+ if (c == '$') {
+ String tag = getDollarQuoteTag(sql, index);
+ if (tag != null) {
+ int end = sql.indexOf(tag, index + tag.length());
+ if (end < 0) {
+ preparedSql.append(sql, index, sql.length());
+ break;
+ }
+ int closeEnd = end + tag.length();
+ preparedSql.append(sql, index, closeEnd);
+ index = closeEnd;
+ continue;
+ }
}
+
+ if (c == '?') {
+ if (index + 1 < sql.length() && sql.charAt(index + 1) == '{') {
+ int end = sql.indexOf('}', index + 2);
+ if (end > index + 2) {
+ String fieldName = sql.substring(index + 2, end).trim();
+ if (!fieldName.isEmpty()) {
+ parameterReferences.add(fieldName);
+ preparedSql.append('?');
+ index = end + 1;
+ continue;
+ }
+ }
+ }
+
+ int jsonbOperatorLength = getPostgresJsonbOperatorLength(sql, index);
+ if (jsonbOperatorLength > 0) {
+ preparedSql.append(sql, index, index + jsonbOperatorLength);
+ index += jsonbOperatorLength;
+ continue;
+ }
+
+ parameterReferences.add(null);
+ preparedSql.append(c);
+ index++;
+ continue;
+ }
+
+ preparedSql.append(c);
+ index++;
+ }
+
+ return new SqlParameterSpec(preparedSql.toString(), parameterReferences);
+ }
+
+ private static int consumeQuotedSection(
+ String sql, int startIndex, char quoteCharacter, boolean
allowDoubledQuoteEscape) {
+ int index = startIndex + 1;
+ while (index < sql.length()) {
+ if (sql.charAt(index) == quoteCharacter) {
+ if (allowDoubledQuoteEscape
+ && index + 1 < sql.length()
+ && sql.charAt(index + 1) == quoteCharacter) {
+ index += 2;
+ continue;
+ }
+ return index + 1;
+ }
+ index++;
}
+ return sql.length();
+ }
- return q;
+ private static int consumeBracketIdentifier(String sql, int startIndex) {
+ int index = startIndex + 1;
+ while (index < sql.length()) {
+ if (sql.charAt(index) == ']') {
+ if (index + 1 < sql.length() && sql.charAt(index + 1) == ']') {
+ index += 2;
+ continue;
+ }
+ return index + 1;
+ }
+ index++;
+ }
+ return sql.length();
+ }
+
+ private static String getDollarQuoteTag(String sql, int startIndex) {
+ if (sql.charAt(startIndex) != '$') {
+ return null;
+ }
+ int cursor = startIndex + 1;
+ while (cursor < sql.length()) {
+ char current = sql.charAt(cursor);
+ if (current == '$') {
+ return sql.substring(startIndex, cursor + 1);
+ }
+ if (!(Character.isLetterOrDigit(current) || current == '_')) {
+ return null;
+ }
+ cursor++;
+ }
+ return null;
+ }
+
+ /**
+ * Keep PostgreSQL JSONB operators (?|, ?&, ??, ??|, ??&) from being parsed
as JDBC parameters.
+ */
+ private static int getPostgresJsonbOperatorLength(String sql, int
questionMarkIndex) {
+ int length = sql.length();
+ if (questionMarkIndex + 2 < length
+ && sql.charAt(questionMarkIndex) == '?'
+ && sql.charAt(questionMarkIndex + 1) == '?'
+ && (sql.charAt(questionMarkIndex + 2) == '|' ||
sql.charAt(questionMarkIndex + 2) == '&')) {
+ return 3;
+ }
+
+ if (questionMarkIndex + 1 < length && sql.charAt(questionMarkIndex) ==
'?') {
+ char next = sql.charAt(questionMarkIndex + 1);
+ if (next == '?') {
+ return 2;
+ }
+ if (next == '|' || next == '&') {
+ return 2;
+ }
+ }
+ return 0;
+ }
+
+ public int countParameters(String sql) {
+ return parseSqlParameterSpec(sql,
supportsBracketQuotedIdentifiers()).getParameterCount();
+ }
+
+ private boolean supportsBracketQuotedIdentifiers() {
+ if (databaseMeta == null || databaseMeta.getIDatabase() == null) {
+ return false;
+ }
+ return "[".equals(databaseMeta.getIDatabase().getStartQuote())
+ && "]".equals(databaseMeta.getIDatabase().getEndQuote());
}
// Get the fields back from an SQL query
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 3ec4b3d3ce..9d7fab271f 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
@@ -181,6 +181,72 @@ class DatabaseTest {
assertTrue(rowMeta.getValueMeta(0).isInteger());
}
+ @Test
+ void parseSqlParameterSpecSkipsOnlyExplicitJsonbOperatorQuestionMarks() {
+ String sql =
+ "SELECT \"?identifier\", $$ ? $$ FROM t /* ? block */ "
+ + "WHERE payload ? 'key' AND options ?| array['a'] AND flags ?&
array['b'] "
+ + "AND route = ?{route} -- ? line\nAND status = ?";
+
+ Database.SqlParameterSpec spec = Database.parseSqlParameterSpec(sql);
+
+ assertEquals(
+ "SELECT \"?identifier\", $$ ? $$ FROM t /* ? block */ "
+ + "WHERE payload ? 'key' AND options ?| array['a'] AND flags ?&
array['b'] "
+ + "AND route = ? -- ? line\nAND status = ?",
+ spec.getPreparedSql());
+ assertEquals(3, spec.getParameterCount());
+ assertNull(spec.getParameterReferences().get(0));
+ assertEquals("route", spec.getParameterReferences().get(1));
+ assertNull(spec.getParameterReferences().get(2));
+ }
+
+ @Test
+ void countParametersTreatsArrayConstructorsAsPositionalParameters() {
+ Database db = new Database(log, variables, meta);
+ String sql = "SELECT 1 WHERE x = ANY(ARRAY[?]) AND y = ARRAY[?, ?]";
+
+ assertEquals(3, db.countParameters(sql));
+ }
+
+ @Test
+ void countParametersUsesBracketIdentifierRulesForBracketQuotingDialects() {
+ when(meta.getIDatabase()).thenReturn(iDatabase);
+ when(iDatabase.getStartQuote()).thenReturn("[");
+ when(iDatabase.getEndQuote()).thenReturn("]");
+
+ Database db = new Database(log, variables, meta);
+ String sql =
+ "SELECT [a?b], `c?d`, \"e?f\" FROM t WHERE payload ? ? AND note = '?z'
"
+ + "AND id = ? -- ? comment\nAND type = ?";
+
+ assertEquals(4, db.countParameters(sql));
+ }
+
+ @Test
+ void countParametersTreatsCommonJdbcQuestionMarksAsParameters() {
+ Database db = new Database(log, variables, meta);
+
+ assertEquals(2, db.countParameters("WHERE name LIKE ? AND status = ?"));
+ assertEquals(2, db.countParameters("WHERE val BETWEEN ? AND ?"));
+ assertEquals(1, db.countParameters("SELECT ? FROM dual"));
+ assertEquals(1, db.countParameters("WHERE col NOT LIKE ? OR active =
true"));
+ }
+
+ @Test
+ void parseSqlParameterSpecHandlesEscapedQuestionJsonbOperators() {
+ String sql =
+ "SELECT * FROM t WHERE col ?? ? "
+ + "AND options ?| array['a'] "
+ + "AND flags ??& array['b']";
+
+ Database.SqlParameterSpec spec = Database.parseSqlParameterSpec(sql);
+
+ assertEquals(sql, spec.getPreparedSql());
+ assertEquals(1, spec.getParameterCount());
+ assertNull(spec.getParameterReferences().get(0));
+ }
+
/**
* When using getLookup calls there is no need to make attempt to retrieve
row set metadata for
* every call. That may bring performance penalty depends on jdbc driver
implementation. For some
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 aa0c89f2c7..7db91d2ac7 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
@@ -27,6 +27,7 @@ import org.apache.hop.core.exception.HopDatabaseException;
import org.apache.hop.core.exception.HopException;
import org.apache.hop.core.exception.HopTransformException;
import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
import org.apache.hop.core.row.RowDataUtil;
import org.apache.hop.core.row.RowMeta;
import org.apache.hop.core.util.Utils;
@@ -47,6 +48,35 @@ public class DatabaseJoin extends
BaseTransform<DatabaseJoinMeta, DatabaseJoinDa
private final ReentrantLock dbLock = new ReentrantLock();
+ /**
+ * Appends runtime output metadata to the given DatabaseJoinData object.
+ *
+ * @param data The DatabaseJoinData object to update.
+ * @param addMeta The additional metadata to append.
+ */
+ private void appendRuntimeOutputMetadata(DatabaseJoinData data, IRowMeta
addMeta) {
+ if (data.outputMetadataInitialized || addMeta == null) {
+ return;
+ }
+
+ for (int i = 0; i < addMeta.size(); i++) {
+ IValueMeta valueMeta = addMeta.getValueMeta(i).clone();
+ String valueName = valueMeta.getName();
+ if (valueName == null || valueName.trim().isEmpty()) {
+ valueName = "field" + (i + 1);
+ }
+ valueMeta.setName(valueName);
+ valueMeta.setOrigin(getTransformName());
+ data.outputRowMeta.addValueMeta(valueMeta);
+ }
+ data.outputMetadataInitialized = true;
+
+ if (isDetailed()) {
+ logDetailed(
+ "Initialized Database Join runtime output metadata: " +
data.outputRowMeta.toString());
+ }
+ }
+
public DatabaseJoin(
TransformMeta transformMeta,
DatabaseJoinMeta meta,
@@ -74,6 +104,9 @@ public class DatabaseJoin extends
BaseTransform<DatabaseJoinMeta, DatabaseJoinDa
null,
this,
metadataProvider);
+ // mark whether getFields populated runtime output meta (for stored
procs it may defer to
+ // runtime)
+ data.outputMetadataInitialized = data.outputRowMeta.size() >
rowMeta.size();
data.lookupRowMeta = new RowMeta();
@@ -83,14 +116,26 @@ public class DatabaseJoin extends
BaseTransform<DatabaseJoinMeta, DatabaseJoinDa
+ rowMeta.getString(rowData));
}
- data.keynrs = new int[meta.getParameters().size()];
+ DatabaseJoinMeta.SqlParameterSpec parameterSpec = data.parameterSpec;
+ if (parameterSpec == null) {
+ throw new HopTransformException(
+ "Database Join SQL parameter specification is not initialized.");
+ }
+ data.keynrs = new int[parameterSpec.getParameterCount()];
+
+ int positionalIndex = 0;
+ for (int i = 0; i < parameterSpec.getParameterCount(); i++) {
+ String parameterReference =
parameterSpec.getParameterReferences().get(i);
+ String sourceFieldName = parameterReference;
+ if (sourceFieldName == null) {
+ sourceFieldName =
meta.getPositionalParameterFieldName(positionalIndex);
+ positionalIndex++;
+ }
- for (int i = 0; i < data.keynrs.length; i++) {
- ParameterField field = meta.getParameters().get(i);
- data.keynrs[i] = rowMeta.indexOfValue(field.getName());
+ data.keynrs[i] = rowMeta.indexOfValue(sourceFieldName);
if (data.keynrs[i] < 0) {
throw new HopTransformException(
- BaseMessages.getString(PKG,
"DatabaseJoin.Exception.FieldNotFound", field.getName()));
+ BaseMessages.getString(PKG,
"DatabaseJoin.Exception.FieldNotFound", sourceFieldName));
}
data.lookupRowMeta.addValueMeta(rowMeta.getValueMeta(data.keynrs[i]).clone());
@@ -107,6 +152,10 @@ public class DatabaseJoin extends
BaseTransform<DatabaseJoinMeta, DatabaseJoinDa
IRowMeta addMeta = data.db.getReturnRowMeta();
+ // If we couldn't determine output fields at design time, derive them
from the runtime
+ // resultset
+ appendRuntimeOutputMetadata(data, addMeta);
+
int counter = 0;
for (Object[] add : adds) {
if (add != null && (meta.getRowLimit() == 0 || counter <
meta.getRowLimit())) {
@@ -277,10 +326,18 @@ public class DatabaseJoin extends
BaseTransform<DatabaseJoinMeta, DatabaseJoinDa
if (meta.isReplaceVariables()) {
sql = resolve(sql);
}
+
+ // Parse SQL parameter spec (supports ?{name}) and prepare statement
with prepared SQL
+ DatabaseJoinMeta.SqlParameterSpec parameterSpec =
+ DatabaseJoinMeta.parseSqlParameterSpec(
+ sql,
DatabaseJoinMeta.supportsBracketQuotedIdentifiers(databaseMeta));
+ data.parameterSpec = parameterSpec;
+ String preparedSql = parameterSpec.getPreparedSql();
+
// Prepare the SQL statement
- data.pstmt = data.db.prepareSql(sql);
+ data.pstmt = data.db.prepareSql(preparedSql);
if (isDebug()) {
- logDebug(BaseMessages.getString(PKG,
"DatabaseJoin.Log.SQLStatement", sql));
+ logDebug(BaseMessages.getString(PKG,
"DatabaseJoin.Log.SQLStatement", preparedSql));
}
data.db.setQueryLimit(meta.getRowLimit());
diff --git
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinData.java
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinData.java
index 52fcfec614..7e2c5a481b 100644
---
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinData.java
+++
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinData.java
@@ -33,7 +33,13 @@ public class DatabaseJoinData extends BaseTransformData
implements ITransformDat
IRowMeta outputRowMeta;
IRowMeta lookupRowMeta;
+ // If SQL contains named and/or positional parameters, this spec describes
them
+ public DatabaseJoinMeta.SqlParameterSpec parameterSpec;
+
public int[] keynrs; // parameter value index in an input row...
public Object[] notfound; // Values in case nothing is found...
public boolean isCanceled;
+
+ // The output metadata has been initialized.
+ public boolean outputMetadataInitialized;
}
diff --git
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
index 32d783a079..c655466284 100644
---
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
+++
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
@@ -20,7 +20,9 @@ package org.apache.hop.pipeline.transforms.databasejoin;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import org.apache.hop.core.Const;
import org.apache.hop.core.Props;
import org.apache.hop.core.database.DatabaseMeta;
@@ -81,16 +83,20 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
private Button wOuter;
private TableView wParam;
+ private TableView wResolvedParam;
private Button wUseVars;
private final DatabaseJoinMeta input;
private Label wlPosition;
+ private Label wlParam;
private ColumnInfo[] ciKey;
+ private ColumnInfo[] ciResolvedParam;
private final List<String> inputFields = new ArrayList<>();
+ private IRowMeta sourceFieldsMeta;
private Button wCache;
@@ -117,7 +123,12 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
// Connection line
wConnection = addConnectionLine(shell, wSpacer, input.getConnection(),
lsMod);
- wConnection.addListener(SWT.Selection, e -> getSqlReservedWords());
+ wConnection.addListener(
+ SWT.Selection,
+ e -> {
+ getSqlReservedWords();
+ refreshResolvedParametersPanel();
+ });
// ICache?
Label wlCache = new Label(shell, SWT.RIGHT);
@@ -228,10 +239,12 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
wSql.addLineStyleListener(getSqlReservedWords());
PropsUi.setLook(wSql, Props.WIDGET_STYLE_FIXED);
wSql.addModifyListener(lsMod);
+ wSql.addModifyListener(e -> refreshResolvedParametersPanel());
wSqlFromFile.addModifyListener(
e -> {
if (Utils.isEmpty(wSqlFromFile.getText())) {
wSql.setEditable(true);
+ refreshResolvedParametersPanel();
}
});
FormData fdSql = new FormData();
@@ -293,6 +306,53 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
fdlPosition.right = new FormAttachment(100, 0);
wlPosition.setLayoutData(fdlPosition);
+ Label wlResolvedParam = new Label(shell, SWT.NONE);
+ wlResolvedParam.setText(
+ BaseMessages.getString(PKG,
"DatabaseJoinDialog.ResolvedParameters.Label"));
+ PropsUi.setLook(wlResolvedParam);
+ FormData fdlResolvedParam = new FormData();
+ fdlResolvedParam.left = new FormAttachment(0, 0);
+ fdlResolvedParam.top = new FormAttachment(wlPosition, margin);
+ wlResolvedParam.setLayoutData(fdlResolvedParam);
+
+ ciResolvedParam = new ColumnInfo[3];
+ ciResolvedParam[0] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"DatabaseJoinDialog.ColumnInfo.ResolvedPlaceholder"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false);
+ ciResolvedParam[1] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"DatabaseJoinDialog.ColumnInfo.ResolvedInputField"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false);
+ ciResolvedParam[2] =
+ new ColumnInfo(
+ BaseMessages.getString(PKG,
"DatabaseJoinDialog.ColumnInfo.ResolvedType"),
+ ColumnInfo.COLUMN_TYPE_TEXT,
+ false);
+
+ wResolvedParam =
+ new TableView(
+ variables,
+ shell,
+ SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL |
SWT.H_SCROLL,
+ ciResolvedParam,
+ 1,
+ true,
+ e -> {},
+ props,
+ true,
+ null,
+ false,
+ false);
+ FormData fdResolvedParam = new FormData();
+ fdResolvedParam.left = new FormAttachment(0, 0);
+ fdResolvedParam.top = new FormAttachment(wlResolvedParam, margin);
+ fdResolvedParam.right = new FormAttachment(100, 0);
+ fdResolvedParam.height = (int) (90 * props.getZoomFactor());
+ wResolvedParam.setLayoutData(fdResolvedParam);
+
// Limit the number of lines returns
Label wlLimit = new Label(shell, SWT.RIGHT);
wlLimit.setText(BaseMessages.getString(PKG,
"DatabaseJoinDialog.Limit.Label"));
@@ -300,7 +360,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
FormData fdlLimit = new FormData();
fdlLimit.left = new FormAttachment(0, 0);
fdlLimit.right = new FormAttachment(middle, -margin);
- fdlLimit.top = new FormAttachment(wlPosition, margin);
+ fdlLimit.top = new FormAttachment(wResolvedParam, margin);
wlLimit.setLayoutData(fdlLimit);
wLimit = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
PropsUi.setLook(wLimit);
@@ -308,7 +368,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
FormData fdLimit = new FormData();
fdLimit.left = new FormAttachment(middle, 0);
fdLimit.right = new FormAttachment(100, 0);
- fdLimit.top = new FormAttachment(wlPosition, margin);
+ fdLimit.top = new FormAttachment(wResolvedParam, margin);
wLimit.setLayoutData(fdLimit);
// Outer join?
@@ -363,7 +423,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
// THE BUTTONS
// The parameters
- Label wlParam = new Label(shell, SWT.NONE);
+ wlParam = new Label(shell, SWT.NONE);
wlParam.setText(BaseMessages.getString(PKG,
"DatabaseJoinDialog.Param.Label"));
PropsUi.setLook(wlParam);
FormData fdlParam = new FormData();
@@ -387,6 +447,12 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
ColumnInfo.COLUMN_TYPE_CCOMBO,
ValueMetaFactory.getValueMetaNames());
+ ModifyListener lsParamMod =
+ e -> {
+ input.setChanged();
+ refreshResolvedParametersPanel();
+ };
+
wParam =
new TableView(
variables,
@@ -394,7 +460,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
SWT.BORDER | SWT.FULL_SELECTION | SWT.MULTI | SWT.V_SCROLL |
SWT.H_SCROLL,
ciKey,
nrKeyRows,
- lsMod,
+ lsParamMod,
props);
FormData fdParam = new FormData();
@@ -415,10 +481,14 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
IRowMeta row = pipelineMeta.getPrevTransformFields(variables,
transformMeta);
// Remember these fields...
+ sourceFieldsMeta = row;
for (int i = 0; i < row.size(); i++) {
inputFields.add(row.getValueMeta(i).getName());
}
setComboBoxes();
+ if (!shell.isDisposed()) {
+ shell.getDisplay().asyncExec(() ->
refreshResolvedParametersPanel());
+ }
} catch (HopException e) {
logError(BaseMessages.getString(PKG,
"System.Dialog.GetFieldsFailed.Message"));
}
@@ -471,6 +541,114 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
PKG, "DatabaseJoinDialog.Position.Label", "" + lineNumber, "" +
columnNumber));
}
+ private DatabaseJoinMeta.SqlParameterSpec parseCurrentSqlParameterSpec() {
+ String sourceSql = wSql == null ? null : wSql.getText();
+ return DatabaseJoinMeta.parseSqlParameterSpec(
+ sourceSql,
+ DatabaseJoinMeta.supportsBracketQuotedIdentifiers(
+ pipelineMeta.findDatabase(wConnection.getText(), variables)));
+ }
+
+ private List<ParameterField> getDeclaredParametersFromGrid() {
+ List<ParameterField> parameters = new ArrayList<>();
+ if (wParam == null) {
+ return parameters;
+ }
+
+ int nrparam = wParam.nrNonEmpty();
+ for (int i = 0; i < nrparam; i++) {
+ TableItem item = wParam.getNonEmpty(i);
+ ParameterField field = new ParameterField();
+ field.setName(item.getText(1));
+ field.setType(ValueMetaFactory.getIdForValueMeta(item.getText(2)));
+ parameters.add(field);
+ }
+ return parameters;
+ }
+
+ private void refreshResolvedParametersPanel() {
+ if (wResolvedParam == null || wResolvedParam.isDisposed()) {
+ return;
+ }
+
+ DatabaseJoinMeta.SqlParameterSpec parameterSpec =
parseCurrentSqlParameterSpec();
+ List<ParameterField> declaredParameters = getDeclaredParametersFromGrid();
+ Map<String, String> declaredTypesByName = new LinkedHashMap<>();
+ for (ParameterField parameter : declaredParameters) {
+ if (!Utils.isEmpty(parameter.getName())) {
+ declaredTypesByName.put(parameter.getName(), parameter.getType());
+ }
+ }
+
+ wResolvedParam.table.removeAll();
+ int positionalIndex = 0;
+ for (String parameterReference : parameterSpec.getParameterReferences()) {
+ boolean positional = parameterReference == null;
+ String placeholder = positional ? "?" : "?{" + parameterReference + "}";
+
+ String inputFieldName = parameterReference;
+ String declaredType = positional ? null :
declaredTypesByName.get(parameterReference);
+ if (positional) {
+ if (positionalIndex < declaredParameters.size()) {
+ ParameterField declared = declaredParameters.get(positionalIndex);
+ inputFieldName = declared.getName();
+ declaredType = declared.getType();
+ } else {
+ inputFieldName = null;
+ }
+ positionalIndex++;
+ }
+
+ boolean foundInInput =
+ !Utils.isEmpty(inputFieldName)
+ && ((sourceFieldsMeta != null &&
sourceFieldsMeta.indexOfValue(inputFieldName) >= 0)
+ || inputFields.contains(inputFieldName));
+
+ String inputFieldDisplay;
+ if (Utils.isEmpty(inputFieldName)) {
+ inputFieldDisplay =
+ BaseMessages.getString(PKG,
"DatabaseJoinDialog.ResolvedParameters.Unmapped");
+ } else if (foundInInput) {
+ inputFieldDisplay = inputFieldName;
+ } else {
+ inputFieldDisplay =
+ inputFieldName
+ + " ("
+ + BaseMessages.getString(PKG,
"DatabaseJoinDialog.ResolvedParameters.NotFound")
+ + ")";
+ }
+
+ String typeDisplay = declaredType;
+ if (foundInInput && sourceFieldsMeta != null) {
+ int sourceIndex = sourceFieldsMeta.indexOfValue(inputFieldName);
+ if (sourceIndex >= 0) {
+ typeDisplay =
+ ValueMetaFactory.getValueMetaName(
+ sourceFieldsMeta.getValueMeta(sourceIndex).getType());
+ }
+ }
+ if (typeDisplay == null) {
+ typeDisplay = "";
+ }
+
+ wResolvedParam.add(placeholder, inputFieldDisplay, typeDisplay);
+ }
+
+ if (wResolvedParam.table.getItemCount() == 0) {
+ new TableItem(wResolvedParam.table, SWT.NONE);
+ }
+ wResolvedParam.setRowNums();
+ wResolvedParam.optWidth(true);
+
+ boolean hasPositionalParameters =
parameterSpec.getPositionalParameterCount() > 0;
+ if (wParam != null && !wParam.isDisposed()) {
+ wParam.setEnabled(hasPositionalParameters);
+ }
+ if (wlParam != null && !wlParam.isDisposed()) {
+ wlParam.setEnabled(hasPositionalParameters);
+ }
+ }
+
private void loadSqlFromFileAndSetReadOnly() {
String path = variables.resolve(wSqlFromFile.getText());
if (Utils.isEmpty(path)) {
@@ -481,6 +659,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
String content = HopVfs.getTextFileContent(path, StandardCharsets.UTF_8);
wSql.setText(content);
wSql.setEditable(false);
+ refreshResolvedParametersPanel();
} catch (HopFileException e) {
MessageBox mb = new MessageBox(shell, SWT.OK | SWT.ICON_WARNING);
mb.setText(BaseMessages.getString(PKG,
"DatabaseJoinDialog.InvalidConnection.DialogTitle"));
@@ -490,6 +669,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
+ e.getMessage());
mb.open();
wSql.setEditable(true);
+ refreshResolvedParametersPanel();
}
}
@@ -525,6 +705,7 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
wParam.setRowNums();
wParam.optWidth(true);
+ refreshResolvedParametersPanel();
enableFields();
}
@@ -540,8 +721,6 @@ public class DatabaseJoinDialog extends BaseTransformDialog
{
return;
}
- int nrparam = wParam.nrNonEmpty();
-
input.setConnection(wConnection.getText());
input.setCached(wCache.getSelection());
input.setCacheSize(Const.toInt(wCacheSize.getText(), 0));
@@ -550,19 +729,11 @@ public class DatabaseJoinDialog extends
BaseTransformDialog {
input.setSqlFromFile(wSqlFromFile.getText());
input.setOuterJoin(wOuter.getSelection());
input.setReplaceVariables(wUseVars.getSelection());
+ List<ParameterField> parameters = getDeclaredParametersFromGrid();
logDebug(
BaseMessages.getString(PKG, "DatabaseJoinDialog.Log.ParametersFound")
- + nrparam
+ + parameters.size()
+ " parameters");
-
- List<ParameterField> parameters = new ArrayList<>();
- for (int i = 0; i < nrparam; i++) {
- TableItem item = wParam.getNonEmpty(i);
- ParameterField field = new ParameterField();
- field.setName(item.getText(1));
- field.setType(ValueMetaFactory.getIdForValueMeta(item.getText(2)));
- parameters.add(field);
- }
input.setParameters(parameters);
transformName = wTransformName.getText(); // return value
diff --git
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
index 32cebec8a4..7499620a57 100644
---
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
+++
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMeta.java
@@ -161,21 +161,235 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
parameters = new ArrayList<>();
}
- public IRowMeta getParameterRow(IRowMeta fields) {
- IRowMeta param = new RowMeta();
+ /**
+ * SQL parameter specification parsed from source SQL. Supports named
?{name} and positional ?
+ * placeholders.
+ */
+ static final class SqlParameterSpec {
+ private final String preparedSql;
+ private final List<String> parameterReferences;
+ private final int positionalParameterCount;
+
+ SqlParameterSpec(
+ String preparedSql, List<String> parameterReferences, int
positionalParameterCount) {
+ this.preparedSql = preparedSql;
+ this.parameterReferences = parameterReferences;
+ this.positionalParameterCount = positionalParameterCount;
+ }
+
+ String getPreparedSql() {
+ return preparedSql;
+ }
+
+ List<String> getParameterReferences() {
+ return parameterReferences;
+ }
+
+ int getParameterCount() {
+ return parameterReferences.size();
+ }
- if (fields != null) {
- for (ParameterField field : this.parameters) {
- IValueMeta valueMeta = fields.searchValueMeta(field.getName());
- if (valueMeta != null) {
- param.addValueMeta(valueMeta);
+ int getPositionalParameterCount() {
+ return positionalParameterCount;
+ }
+
+ boolean hasNamedParameters() {
+ for (String parameterReference : parameterReferences) {
+ if (parameterReference != null) {
+ return true;
}
}
+ return false;
}
+ }
+
+ public IRowMeta createQueryParameterRowMeta(IRowMeta sourceMeta,
SqlParameterSpec parameterSpec)
+ throws HopTransformException {
+ IRowMeta queryParametersMeta = new RowMeta();
+
+ int positionalIndex = 0;
+ for (String parameterReference : parameterSpec.getParameterReferences()) {
+ String sourceFieldName = parameterReference;
+ if (sourceFieldName == null) {
+ sourceFieldName = getPositionalParameterFieldName(positionalIndex);
+ positionalIndex++;
+ }
+ if (Utils.isEmpty(sourceFieldName)) {
+ throw new HopTransformException(
+ "Unable to find input field for positional SQL parameter #"
+ + positionalIndex
+ + ". Configure parameter fields or use ?{fieldName}.");
+ }
+
+ int sourceIndex = sourceMeta == null ? -1 :
sourceMeta.indexOfValue(sourceFieldName);
+ if (sourceIndex < 0 || sourceIndex >= sourceMeta.size()) {
+ throw new HopTransformException(
+ "Unable to find input field [" + sourceFieldName + "] referenced
in SQL parameter.");
+ }
+
+
queryParametersMeta.addValueMeta(sourceMeta.getValueMeta(sourceIndex).clone());
+ }
+
+ return queryParametersMeta;
+ }
+
+ /**
+ * Parse SQL with support for ?{name} named placeholders. Returns prepared
SQL (with '?' markers)
+ * and a list of parameter references (null for positional parameters).
+ */
+ static SqlParameterSpec parseSqlParameterSpec(String sourceSql) {
+ return parseSqlParameterSpec(sourceSql, false);
+ }
+
+ static SqlParameterSpec parseSqlParameterSpec(
+ String sourceSql, boolean consumeBracketIdentifiers) {
+ Database.SqlParameterSpec parsed =
+ Database.parseSqlParameterSpec(sourceSql, consumeBracketIdentifiers);
+ int positionalParameterCount = 0;
+ for (String parameterReference : parsed.getParameterReferences()) {
+ if (parameterReference == null) {
+ positionalParameterCount++;
+ }
+ }
+ return new SqlParameterSpec(
+ parsed.getPreparedSql(), parsed.getParameterReferences(),
positionalParameterCount);
+ }
+
+ static boolean supportsBracketQuotedIdentifiers(DatabaseMeta databaseMeta) {
+ if (databaseMeta == null || databaseMeta.getIDatabase() == null) {
+ return false;
+ }
+ return "[".equals(databaseMeta.getIDatabase().getStartQuote())
+ && "]".equals(databaseMeta.getIDatabase().getEndQuote());
+ }
+
+ static java.util.Set<String> getMissingNamedParameters(
+ IRowMeta sourceMeta, SqlParameterSpec parameterSpec) {
+ java.util.Set<String> missingFields = new java.util.LinkedHashSet<>();
+
+ for (String parameterReference : parameterSpec.getParameterReferences()) {
+ if (parameterReference != null
+ && (sourceMeta == null ||
sourceMeta.indexOfValue(parameterReference) < 0)) {
+ missingFields.add(parameterReference);
+ }
+ }
+
+ return missingFields;
+ }
+
+ /** Get the missing positional parameter fields for the given parameter
specification. */
+ java.util.Set<String> getMissingPositionalParameterFields(
+ IRowMeta sourceMeta, SqlParameterSpec parameterSpec) {
+ java.util.Set<String> missingFields = new java.util.LinkedHashSet<>();
+ for (int i = 0; i < parameterSpec.getPositionalParameterCount(); i++) {
+ String positionalFieldName = getPositionalParameterFieldName(i);
+ if (Utils.isEmpty(positionalFieldName)) {
+ missingFields.add("#" + (i + 1));
+ } else if (sourceMeta == null ||
sourceMeta.indexOfValue(positionalFieldName) < 0) {
+ missingFields.add(positionalFieldName);
+ }
+ }
+ return missingFields;
+ }
+
+ /**
+ * Heuristic to determine if the SQL is likely a stored procedure call. Used
to suppress metadata
+ * discovery errors at design time.
+ */
+ private boolean isLikelyStoredProcedureSql(String sqlText) {
+ String normalized = Const.NVL(sqlText, "").trim().toLowerCase();
+ return normalized.startsWith("exec ")
+ || normalized.startsWith("execute ")
+ || normalized.startsWith("{call ");
+ }
+
+ /**
+ * Get the field name for the positional parameter at the given index.
Returns null if no field is
+ * defined for that index.
+ */
+ String getPositionalParameterFieldName(int positionalIndex) {
+ if (parameters == null || positionalIndex < 0 || positionalIndex >=
parameters.size()) {
+ return null;
+ }
+ return parameters.get(positionalIndex).getName();
+ }
+
+ /** Build a row meta describing declared parameters (from the step
configuration). */
+ public IRowMeta createDeclaredParameterRowMeta() {
+ IRowMeta param = new RowMeta();
+ if (parameters == null) {
+ return param;
+ }
+
+ for (ParameterField field : this.parameters) {
+ IValueMeta v;
+ try {
+ int id = ValueMetaFactory.getIdForValueMeta(field.getType());
+ v = ValueMetaFactory.createValueMeta(field.getName(), id);
+ } catch (HopPluginException e) {
+ v = new ValueMetaNone(field.getName());
+ }
+ param.addValueMeta(v);
+ }
return param;
}
+ /**
+ * Create a parameter row meta suitable for metadata discovery: maps named
placeholders to
+ * declared parameters when possible and falls back to ValueMetaNone
placeholders when not
+ * available.
+ */
+ public IRowMeta createMetadataLookupParameterRowMeta(SqlParameterSpec
parameterSpec) {
+ return createMetadataLookupParameterRowMeta(parameterSpec, null);
+ }
+
+ /**
+ * Create a parameter row meta suitable for metadata discovery: maps
placeholders to incoming
+ * stream fields when available and falls back to declared parameter types.
+ */
+ public IRowMeta createMetadataLookupParameterRowMeta(
+ SqlParameterSpec parameterSpec, IRowMeta sourceMeta) {
+ IRowMeta declared = createDeclaredParameterRowMeta();
+ IRowMeta queryParameters = new RowMeta();
+
+ int positionalIndex = 0;
+ for (String parameterReference : parameterSpec.getParameterReferences()) {
+ String lookupFieldName = parameterReference;
+ String defaultName = lookupFieldName;
+ if (lookupFieldName == null) {
+ lookupFieldName = getPositionalParameterFieldName(positionalIndex);
+ defaultName = "param" + (positionalIndex + 1);
+ }
+
+ IValueMeta valueMeta = null;
+ if (!Utils.isEmpty(lookupFieldName) && sourceMeta != null) {
+ valueMeta = sourceMeta.searchValueMeta(lookupFieldName);
+ }
+ if (valueMeta == null && !Utils.isEmpty(lookupFieldName)) {
+ valueMeta = declared.searchValueMeta(lookupFieldName);
+ }
+ if (valueMeta == null) {
+ valueMeta = new ValueMetaNone(defaultName);
+ }
+
+ queryParameters.addValueMeta(valueMeta.clone());
+ if (parameterReference == null) {
+ positionalIndex++;
+ }
+ }
+
+ return queryParameters;
+ }
+
+ /** Create a parameter row suitable for metadata discovery with null values.
*/
+ public Object[] createMetadataLookupParameterRowData(IRowMeta
queryParametersMeta) {
+ if (queryParametersMeta == null || queryParametersMeta.isEmpty()) {
+ return new Object[0];
+ }
+ return new Object[queryParametersMeta.size()];
+ }
+
@Override
public void getFields(
IRowMeta row,
@@ -207,8 +421,6 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
// Which fields are parameters?
// info[0] comes from the database connection.
//
- IRowMeta param = getParameterRow(row);
-
String sqlToUse;
try {
sqlToUse = resolveSql(variables);
@@ -216,17 +428,30 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
throw new HopTransformException(e.getMessage(), e);
}
+ SqlParameterSpec parameterSpec =
+ parseSqlParameterSpec(sqlToUse,
supportsBracketQuotedIdentifiers(databaseMeta));
+
+ // Build metadata lookup parameters, preferring incoming field types
when available.
+ IRowMeta param = createMetadataLookupParameterRowMeta(parameterSpec,
row);
+ Object[] paramRow = createMetadataLookupParameterRowData(param);
+
// First try without connecting to the database... (can be S L O W)
// See if it's in the cache...
//
IRowMeta add = null;
try {
- add = db.getQueryFields(sqlToUse, true, param, new
Object[param.size()]);
+ add = db.getQueryFields(parameterSpec.getPreparedSql(), true, param,
paramRow);
} catch (HopDatabaseException dbe) {
+ if (isLikelyStoredProcedureSql(parameterSpec.getPreparedSql())) {
+ logDetailed(
+ "Unable to determine stored procedure output fields at design
time; deferring metadata to runtime.");
+ logDebug("Stored procedure metadata discovery exception", dbe);
+ return;
+ }
throw new HopTransformException(
BaseMessages.getString(PKG,
"DatabaseJoinMeta.Exception.UnableToDetermineQueryFields")
+ Const.CR
- + sqlToUse,
+ + parameterSpec.getPreparedSql(),
dbe);
}
@@ -241,13 +466,25 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
//
try {
db.connect();
- add = db.getQueryFields(sqlToUse, true, param, new
Object[param.size()]);
+ } catch (HopDatabaseException dbe) {
+ throw new HopTransformException(
+ BaseMessages.getString(PKG,
"DatabaseJoinMeta.Exception.ErrorObtainingFields"), dbe);
+ }
+
+ try {
+ add = db.getQueryFields(parameterSpec.getPreparedSql(), true, param,
paramRow);
for (int i = 0; i < add.size(); i++) {
IValueMeta v = add.getValueMeta(i);
v.setOrigin(name);
}
row.addRowMeta(add);
} catch (HopDatabaseException dbe) {
+ if (isLikelyStoredProcedureSql(parameterSpec.getPreparedSql())) {
+ logDetailed(
+ "Unable to determine stored procedure output fields after
connecting; deferring metadata to runtime.");
+ logDebug("Stored procedure metadata discovery exception after
connecting", dbe);
+ return;
+ }
throw new HopTransformException(
BaseMessages.getString(PKG,
"DatabaseJoinMeta.Exception.ErrorObtainingFields"), dbe);
}
@@ -291,14 +528,30 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
databases = new Database[] {db}; // Keep track of this one for
cancelQuery
try {
- db.connect();
String sqlToUse = resolveSql(variables);
- if (!Utils.isEmpty(sqlToUse)) {
- IRowMeta param = getParameterRow(prev);
+ if (Utils.isEmpty(sqlToUse)) {
+ errorMessage = BaseMessages.getString(PKG,
"DatabaseJoinMeta.CheckResult.InvalidDBQuery");
+ cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage,
transformMeta);
+ remarks.add(cr);
+ } else {
+ db.connect();
+ SqlParameterSpec parameterSpec =
+ parseSqlParameterSpec(sqlToUse,
supportsBracketQuotedIdentifiers(databaseMeta));
errorMessage = "";
- IRowMeta r = db.getQueryFields(sqlToUse, true, param, new
Object[param.size()]);
+ IRowMeta param;
+ try {
+ param = createQueryParameterRowMeta(prev, parameterSpec);
+ } catch (HopTransformException e) {
+ cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR,
e.getMessage(), transformMeta);
+ remarks.add(cr);
+ param = new RowMeta();
+ }
+
+ IRowMeta r =
+ db.getQueryFields(
+ parameterSpec.getPreparedSql(), true, param, new
Object[param.size()]);
if (r != null) {
cr =
new CheckResult(
@@ -313,8 +566,8 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
remarks.add(cr);
}
- int q = db.countParameters(sqlToUse);
- if (q != parameters.size()) {
+ int q = db.countParameters(parameterSpec.getPreparedSql());
+ if (q != parameterSpec.getParameterCount()) {
errorMessage =
BaseMessages.getString(
PKG,
"DatabaseJoinMeta.CheckResult.DismatchBetweenParametersAndQuestion")
@@ -327,7 +580,7 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
errorMessage +=
BaseMessages.getString(
PKG,
"DatabaseJoinMeta.CheckResult.DismatchBetweenParametersAndQuestion3")
- + parameters.size();
+ + parameterSpec.getParameterCount();
cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage,
transformMeta);
remarks.add(cr);
@@ -341,17 +594,19 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
transformMeta);
remarks.add(cr);
}
- }
- // Look up fields in the input stream <prev>
- if (prev != null && !prev.isEmpty()) {
- boolean first = true;
- errorMessage = "";
- boolean errorFound = false;
+ // Look up fields in the input stream <prev>
+ if (prev != null && !prev.isEmpty()) {
+ java.util.Set<String> missingNamedParameters =
+ getMissingNamedParameters(prev, parameterSpec);
+ java.util.Set<String> missingPositionalParameterFields =
+ getMissingPositionalParameterFields(prev, parameterSpec);
- for (ParameterField field : this.parameters) {
- IValueMeta v = prev.searchValueMeta(field.getName());
- if (v == null) {
+ boolean first = true;
+ errorMessage = "";
+ boolean errorFound = false;
+
+ for (String parameterName : missingNamedParameters) {
if (first) {
first = false;
errorMessage +=
@@ -359,25 +614,37 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
+ Const.CR;
}
errorFound = true;
- errorMessage += "\t\t" + field.getName() + Const.CR;
+ errorMessage += "\t\t" + parameterName + Const.CR;
}
- }
- if (errorFound) {
- cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage,
transformMeta);
+
+ for (String positionalReference :
missingPositionalParameterFields) {
+ if (first) {
+ first = false;
+ errorMessage +=
+ BaseMessages.getString(PKG,
"DatabaseJoinMeta.CheckResult.MissingFields")
+ + Const.CR;
+ }
+ errorFound = true;
+ errorMessage += "\t\t" + positionalReference + Const.CR;
+ }
+
+ if (errorFound) {
+ cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR,
errorMessage, transformMeta);
+ } else {
+ cr =
+ new CheckResult(
+ ICheckResult.TYPE_RESULT_OK,
+ BaseMessages.getString(PKG,
"DatabaseJoinMeta.CheckResult.AllFieldsFound"),
+ transformMeta);
+ }
+ remarks.add(cr);
} else {
- cr =
- new CheckResult(
- ICheckResult.TYPE_RESULT_OK,
- BaseMessages.getString(PKG,
"DatabaseJoinMeta.CheckResult.AllFieldsFound"),
- transformMeta);
+ errorMessage =
+ BaseMessages.getString(PKG,
"DatabaseJoinMeta.CheckResult.CounldNotReadFields")
+ + Const.CR;
+ cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage,
transformMeta);
+ remarks.add(cr);
}
- remarks.add(cr);
- } else {
- errorMessage =
- BaseMessages.getString(PKG,
"DatabaseJoinMeta.CheckResult.CounldNotReadFields")
- + Const.CR;
- cr = new CheckResult(ICheckResult.TYPE_RESULT_ERROR, errorMessage,
transformMeta);
- remarks.add(cr);
}
} catch (HopException e) {
errorMessage =
@@ -414,32 +681,37 @@ public class DatabaseJoinMeta extends
BaseTransformMeta<DatabaseJoin, DatabaseJo
@Override
public IRowMeta getTableFields(IVariables variables) {
- // Build a dummy parameter row...
- //
-
DatabaseMeta databaseMeta =
getParentTransformMeta().getParentPipelineMeta().findDatabase(connection,
variables);
- IRowMeta param = new RowMeta();
- for (ParameterField field : this.parameters) {
- IValueMeta v;
- try {
- int id = ValueMetaFactory.getIdForValueMeta(field.getType());
- v = ValueMetaFactory.createValueMeta(field.getName(), id);
- } catch (HopPluginException e) {
- v = new ValueMetaNone(field.getName());
- }
- param.addValueMeta(v);
- }
-
IRowMeta fields = null;
if (databaseMeta != null) {
Database db = new Database(loggingObject, variables, databaseMeta);
databases = new Database[] {db}; // Keep track of this one for
cancelQuery
+ SqlParameterSpec parameterSpec = null;
try {
+ parameterSpec =
+ parseSqlParameterSpec(
+ resolveSql(variables),
supportsBracketQuotedIdentifiers(databaseMeta));
db.connect();
- fields = db.getQueryFields(resolveSql(variables), true, param, new
Object[param.size()]);
+ IRowMeta param = createMetadataLookupParameterRowMeta(parameterSpec);
+ try {
+ fields =
+ db.getQueryFields(
+ parameterSpec.getPreparedSql(),
+ true,
+ param,
+ createMetadataLookupParameterRowData(param));
+ } catch (HopException dbe) {
+ if (isLikelyStoredProcedureSql(parameterSpec.getPreparedSql())) {
+ logDetailed(
+ "Unable to determine stored procedure output fields in
getTableFields(); deferring metadata to runtime.");
+ logDebug("Stored procedure getTableFields metadata discovery
exception", dbe);
+ return null;
+ }
+ throw dbe;
+ }
} catch (HopException dbe) {
logError(
BaseMessages.getString(PKG,
"DatabaseJoinMeta.Log.DatabaseErrorOccurred")
diff --git
a/plugins/transforms/databasejoin/src/main/resources/org/apache/hop/pipeline/transforms/databasejoin/messages/messages_en_US.properties
b/plugins/transforms/databasejoin/src/main/resources/org/apache/hop/pipeline/transforms/databasejoin/messages/messages_en_US.properties
index 90ff031e2c..f0c5b13a7c 100644
---
a/plugins/transforms/databasejoin/src/main/resources/org/apache/hop/pipeline/transforms/databasejoin/messages/messages_en_US.properties
+++
b/plugins/transforms/databasejoin/src/main/resources/org/apache/hop/pipeline/transforms/databasejoin/messages/messages_en_US.properties
@@ -30,6 +30,9 @@ DatabaseJoinDialog.Cache.Label=Enable cache
DatabaseJoinDialog.CacheSize.Label=Cache size in rows (0\: cache everything)
DatabaseJoinDialog.ColumnInfo.ParameterFieldname=Parameter fieldname
DatabaseJoinDialog.ColumnInfo.ParameterType=Parameter Type
+DatabaseJoinDialog.ColumnInfo.ResolvedInputField=Input field
+DatabaseJoinDialog.ColumnInfo.ResolvedPlaceholder=Placeholder
+DatabaseJoinDialog.ColumnInfo.ResolvedType=Type
DatabaseJoinDialog.GetFields.Button=\ &Get Fields
DatabaseJoinDialog.GetFieldsFailed.DialogMessage=Unable to get fields from
previous transforms because of an error
DatabaseJoinDialog.GetFieldsFailed.DialogTitle=Get fields failed
@@ -42,6 +45,9 @@ DatabaseJoinDialog.Outerjoin.Label=Outer join
DatabaseJoinDialog.Outerjoin.Tooltip=Return al least the input row and add
NULLs for the lookup values
DatabaseJoinDialog.Param.Label=The parameters to use\:
DatabaseJoinDialog.Position.Label=Line {0} Column {1}
+DatabaseJoinDialog.ResolvedParameters.Label=Resolved SQL parameters\:
+DatabaseJoinDialog.ResolvedParameters.NotFound=not found
+DatabaseJoinDialog.ResolvedParameters.Unmapped=(not mapped)
DatabaseJoinDialog.Shell.Title=Database join
DatabaseJoinDialog.SQL.Label=SQL
DatabaseJoinDialog.LoadSqlFromFile=Load SQL from file
diff --git
a/plugins/transforms/databasejoin/src/test/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMetaTest.java
b/plugins/transforms/databasejoin/src/test/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMetaTest.java
index ca599feefe..6d9b8e45b6 100644
---
a/plugins/transforms/databasejoin/src/test/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMetaTest.java
+++
b/plugins/transforms/databasejoin/src/test/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinMetaTest.java
@@ -18,6 +18,7 @@ package org.apache.hop.pipeline.transforms.databasejoin;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -31,6 +32,7 @@ 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.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaNumber;
import org.apache.hop.core.row.value.ValueMetaString;
import org.apache.hop.core.variables.Variables;
import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
@@ -192,36 +194,155 @@ class DatabaseJoinMetaTest implements
IInitializer<DatabaseJoinMeta> {
}
@Test
- void getParameterRowSelectsMatchingIncomingFieldsInOrder() {
+ void parseSqlParameterSpecSupportsMixedNamedAndPositionalPlaceholders() {
+ String sql =
+ "SELECT order_id, total FROM orders WHERE customer_id = ?{customer_id}
AND status = ?";
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
DatabaseJoinMeta.parseSqlParameterSpec(sql);
+
+ Assertions.assertEquals(
+ "SELECT order_id, total FROM orders WHERE customer_id = ? AND status =
?",
+ spec.getPreparedSql());
+ Assertions.assertEquals(2, spec.getParameterCount());
+ Assertions.assertEquals(1, spec.getPositionalParameterCount());
+ Assertions.assertEquals("customer_id",
spec.getParameterReferences().get(0));
+ Assertions.assertNull(spec.getParameterReferences().get(1));
+ }
+
+ @Test
+ void parseSqlParameterSpecForSqlServerExecUsesJdbcPositionalMarkers() {
+ String sql = "exec usp_WOAvgCycleTimeByOp @OrderNumber = ?{order},
@RouteName = ?{route};";
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
DatabaseJoinMeta.parseSqlParameterSpec(sql);
+
+ Assertions.assertEquals(
+ "exec usp_WOAvgCycleTimeByOp @OrderNumber = ?, @RouteName = ?;",
spec.getPreparedSql());
+ Assertions.assertEquals(List.of("order", "route"),
spec.getParameterReferences());
+ Assertions.assertEquals(0, spec.getPositionalParameterCount());
+ }
+
+ @Test
+ void
parseSqlParameterSpecIgnoresQuotedCommentedAndJsonbOperatorQuestionMarks() {
+ String sql =
+ "SELECT \"?identifier\" FROM t /* ? block */ WHERE payload ? 'x' -- ?
line\nAND route = ?{route} AND status = ?";
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
DatabaseJoinMeta.parseSqlParameterSpec(sql);
+
+ Assertions.assertEquals(
+ "SELECT \"?identifier\" FROM t /* ? block */ WHERE payload ? 'x' -- ?
line\nAND route = ? AND status = ?",
+ spec.getPreparedSql());
+ Assertions.assertEquals(3, spec.getParameterCount());
+ Assertions.assertEquals(Arrays.asList(null, "route", null),
spec.getParameterReferences());
+ }
+
+ @Test
+ void parseSqlParameterSpecIgnoresPostgresDollarQuotedQuestionMarks() {
+ String sql = "SELECT $$ ? $$, $tag$ ?{ignored} $tag$ FROM dual WHERE id =
?";
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
DatabaseJoinMeta.parseSqlParameterSpec(sql);
+
+ Assertions.assertEquals(sql, spec.getPreparedSql());
+ Assertions.assertEquals(1, spec.getParameterCount());
+ Assertions.assertEquals(Arrays.asList((String) null),
spec.getParameterReferences());
+ }
+
+ @Test
+ void parseSqlParameterSpecCountsPostgresArrayConstructorParameters() {
+ String sql = "SELECT 1 WHERE x = ANY(ARRAY[?]) AND y = ARRAY[?, ?]";
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
DatabaseJoinMeta.parseSqlParameterSpec(sql);
+
+ Assertions.assertEquals(sql, spec.getPreparedSql());
+ Assertions.assertEquals(3, spec.getParameterCount());
+ Assertions.assertEquals(Arrays.asList(null, null, null),
spec.getParameterReferences());
+ }
+
+ @Test
+ void createQueryParameterRowMetaResolvesNamedAndPositionalOrder() throws
Exception {
DatabaseJoinMeta meta = new DatabaseJoinMeta();
- ParameterField id = new ParameterField();
- id.setName("id");
- ParameterField missing = new ParameterField();
- missing.setName("missing");
- ParameterField name = new ParameterField();
- name.setName("name");
- meta.setParameters(List.of(id, missing, name));
+ ParameterField positional = new ParameterField();
+ positional.setName("status");
+ positional.setType(IValueMeta.TYPE_STRING);
+ meta.setParameters(List.of(positional));
- IRowMeta incoming = new RowMeta();
- incoming.addValueMeta(new ValueMetaString("name"));
- incoming.addValueMeta(new ValueMetaInteger("id"));
- incoming.addValueMeta(new ValueMetaString("extra"));
+ IRowMeta source = new RowMeta();
+ source.addValueMeta(new ValueMetaInteger("customer_id"));
+ source.addValueMeta(new ValueMetaString("status"));
- IRowMeta param = meta.getParameterRow(incoming);
- Assertions.assertEquals(2, param.size());
- Assertions.assertEquals("id", param.getValueMeta(0).getName());
- Assertions.assertEquals("name", param.getValueMeta(1).getName());
+ DatabaseJoinMeta.SqlParameterSpec spec =
+ DatabaseJoinMeta.parseSqlParameterSpec(
+ "SELECT 1 FROM dual WHERE customer_id = ?{customer_id} AND status
= ?");
+
+ IRowMeta queryParamMeta = meta.createQueryParameterRowMeta(source, spec);
+ Assertions.assertEquals(2, queryParamMeta.size());
+ Assertions.assertEquals("customer_id",
queryParamMeta.getValueMeta(0).getName());
+ Assertions.assertEquals("status",
queryParamMeta.getValueMeta(1).getName());
}
@Test
- void getParameterRowHandlesNullIncoming() {
+ void
createMetadataLookupParameterRowMetaPrefersIncomingTypeOverDeclaredType() {
DatabaseJoinMeta meta = new DatabaseJoinMeta();
ParameterField field = new ParameterField();
field.setName("id");
- meta.getParameters().add(field);
- IRowMeta param = meta.getParameterRow(null);
- Assertions.assertNotNull(param);
- Assertions.assertTrue(param.isEmpty());
+ field.setType(IValueMeta.TYPE_STRING);
+ meta.setParameters(List.of(field));
+
+ IRowMeta incoming = new RowMeta();
+ incoming.addValueMeta(new ValueMetaNumber("id"));
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
+ DatabaseJoinMeta.parseSqlParameterSpec("SELECT 1 FROM dual WHERE id =
?");
+ IRowMeta queryParamMeta = meta.createMetadataLookupParameterRowMeta(spec,
incoming);
+
+ Assertions.assertEquals(1, queryParamMeta.size());
+ Assertions.assertEquals(IValueMeta.TYPE_NUMBER,
queryParamMeta.getValueMeta(0).getType());
+ }
+
+ @Test
+ void createMetadataLookupParameterRowDataUsesNullValues() {
+ DatabaseJoinMeta meta = new DatabaseJoinMeta();
+ ParameterField intField = new ParameterField();
+ intField.setName("order");
+ intField.setType(IValueMeta.TYPE_INTEGER);
+ ParameterField boolField = new ParameterField();
+ boolField.setName("active");
+ boolField.setType(IValueMeta.TYPE_BOOLEAN);
+ ParameterField textField = new ParameterField();
+ textField.setName("route");
+ textField.setType(IValueMeta.TYPE_STRING);
+ meta.setParameters(List.of(intField, boolField, textField));
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
+ DatabaseJoinMeta.parseSqlParameterSpec(
+ "exec usp_WOAvgCycleTimeByOp @OrderNumber = ?{order}, @Active =
?{active}, @RouteName = ?{route}");
+ IRowMeta queryParamMeta = meta.createMetadataLookupParameterRowMeta(spec);
+ Object[] rowData =
meta.createMetadataLookupParameterRowData(queryParamMeta);
+
+ Assertions.assertEquals(3, rowData.length);
+ Assertions.assertNull(rowData[0]);
+ Assertions.assertNull(rowData[1]);
+ Assertions.assertNull(rowData[2]);
+ }
+
+ @Test
+ void
getMissingPositionalParameterFieldsChecksDeclaredFieldAndIncomingFieldPresence()
{
+ DatabaseJoinMeta meta = new DatabaseJoinMeta();
+ ParameterField p1 = new ParameterField();
+ p1.setName("id");
+ ParameterField p2 = new ParameterField();
+ p2.setName("missing");
+ meta.setParameters(List.of(p1, p2));
+
+ IRowMeta incoming = new RowMeta();
+ incoming.addValueMeta(new ValueMetaInteger("id"));
+
+ DatabaseJoinMeta.SqlParameterSpec spec =
+ DatabaseJoinMeta.parseSqlParameterSpec(
+ "SELECT 1 FROM dual WHERE id = ? AND status = ? AND route = ?");
+
+ Assertions.assertEquals(
+ Arrays.asList("missing", "#3"),
+ new ArrayList<>(meta.getMissingPositionalParameterFields(incoming,
spec)));
}
public class ParameterFieldLoadSaveValidator implements
IFieldLoadSaveValidator<ParameterField> {