Skip to content

Commit 7e980e8

Browse files
HCK-14248: apply to instance (#72)
<!--do not remove this marker, its needed to replace info when ticket title is updated --> <!--jira-description-action-hidden-marker-start--> <table> <td> <a href="https://hackolade.atlassian.net/browse/HCK-14248" title="HCK-14248" target="_blank"><img alt="Sub-task" src="https://hackolade.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium" />HCK-14248</a> Add "Apply to Instance" action </td></table> <br /> <!--jira-description-action-hidden-marker-end--> ## Content * enabled "Apply Script" feature with appropriate API handlers <img width="675" height="230" alt="image" src="https://github.com/user-attachments/assets/f15577c5-9939-4673-a451-b3ee7dbc6a34" /> ## Technical details * refactored the approach of passing the connection info and query to java driver via `stdin` * invoked the [reorg table](5306357#diff-ae52694ee7be972bc9173b75ac97bac32d1e41fa05a7250a5f613fb19a4665baR90) while applying the script, as the `CREATE` -> then `ALTER` statements weren't processed correctly in a single script --------- Co-authored-by: Vitalii Bedletskyi <70570504+VitaliiBedletskyi@users.noreply.github.com>
1 parent 0a6b2cf commit 7e980e8

File tree

12 files changed

+307
-209
lines changed

12 files changed

+307
-209
lines changed

forward_engineering/api.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const { generateContainerScript } = require('./api/generateContainerScript');
22
const { isDropInStatements } = require('./api/isDropInStatements');
3+
const { testConnection } = require('../shared/api/testConnection');
4+
const { applyToInstance } = require('./api/applyToInstance');
35

46
module.exports = {
57
generateScript(data, logger, callback, app) {
@@ -16,13 +18,9 @@ module.exports = {
1618
throw new Error('Not implemented');
1719
},
1820

19-
applyToInstance(connectionInfo, logger, callback, app) {
20-
throw new Error('Not implemented');
21-
},
21+
applyToInstance,
2222

23-
testConnection(connectionInfo, logger, callback, app) {
24-
throw new Error('Not implemented');
25-
},
23+
testConnection,
2624

2725
isDropInStatements,
2826
};
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const { logHelper } = require('../../shared/helpers/logHelper');
2+
const { connectionHelper } = require('../../shared/helpers/connectionHelper');
3+
const { instanceHelper } = require('../../shared/helpers/instanceHelper');
4+
5+
async function applyToInstance(connectionInfo, logger, callback, app) {
6+
const applyToInstanceLogger = logHelper.createLogger({
7+
title: 'Apply to instance',
8+
hiddenKeys: connectionInfo.hiddenKeys,
9+
logger,
10+
});
11+
12+
try {
13+
const connection = await connectionHelper.connect({ connectionInfo, logger: applyToInstanceLogger });
14+
await instanceHelper.executeQuery({ connection, query: connectionInfo.script, ddl: true });
15+
16+
callback();
17+
} catch (err) {
18+
applyToInstanceLogger.error(err);
19+
callback(err);
20+
}
21+
}
22+
23+
module.exports = { applyToInstance };

forward_engineering/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
}
1010
],
1111
"hasUpdateScript": false,
12-
"applyScriptToInstance": false,
12+
"applyScriptToInstance": true,
1313
"combinedContainers": true,
1414
"feLevelSelector": {
1515
"container": true,

reverse_engineering/api.js

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { instanceHelper } = require('../shared/helpers/instanceHelper');
1313
const { logHelper } = require('../shared/helpers/logHelper');
1414
const { TABLE_TYPE } = require('../constants/constants');
1515
const { nameHelper } = require('../shared/helpers/nameHelper');
16+
const { testConnection } = require('../shared/api/testConnection');
1617

1718
/**
1819
* @param {ConnectionInfo} connectionInfo
@@ -35,34 +36,6 @@ const disconnect = async (connectionInfo, appLogger, callback) => {
3536
}
3637
};
3738

38-
/**
39-
* @param {ConnectionInfo} connectionInfo
40-
* @param {AppLogger} appLogger
41-
* @param {Callback} callback
42-
* @param {App} app
43-
*/
44-
const testConnection = async (connectionInfo, appLogger, callback, app) => {
45-
const logger = logHelper.createLogger({
46-
title: 'Test database connection',
47-
hiddenKeys: connectionInfo.hiddenKeys,
48-
logger: appLogger,
49-
});
50-
51-
try {
52-
logger.info(connectionInfo);
53-
54-
const connection = await connectionHelper.connect({ connectionInfo, logger });
55-
const version = await instanceHelper.getDbVersion({ connection });
56-
await connectionHelper.disconnect();
57-
58-
logger.info('Db version: ' + version);
59-
callback();
60-
} catch (error) {
61-
logger.error(error);
62-
callback(error);
63-
}
64-
};
65-
6639
/**
6740
* @param {ConnectionInfo} connectionInfo
6841
* @param {AppLogger} appLogger

shared/Db2Client/src/main/java/org/db2/App.java

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,63 @@
11
package org.db2;
22

3-
import org.json.JSONArray;
43
import org.json.JSONObject;
54

6-
import java.sql.SQLException;
7-
import java.util.Arrays;
5+
import java.io.*;
6+
import java.util.stream.Collectors;
87

98
public class App {
109
public static void main(String[] args) {
11-
String host = findArgument(args, Argument.HOST);
12-
String port = findArgument(args, Argument.PORT);
13-
String database = findArgument(args, Argument.DATABASE);
14-
String user = findArgument(args, Argument.USER);
15-
String password = findArgument(args, Argument.PASSWORD);
16-
String query = cleanStringValue(findArgument(args, Argument.QUERY));
17-
String callable = findArgument(args, Argument.CALLABLE);
18-
String inParam = findArgument(args, Argument.IN_PARAM);
19-
20-
Db2Service db2Service = new Db2Service(host, port, database, user, password, new ResponseMapper());
21-
2210
JSONObject result = new JSONObject();
11+
String query = "";
12+
Db2Service db2Service = null;
2313

2414
try {
25-
db2Service.openConnection();
15+
String jsonInput = readStdin();
16+
JSONObject input = new JSONObject(jsonInput);
2617

27-
boolean isCallableQuery = Boolean.parseBoolean(callable);
18+
String host = input.optString("host", "");
19+
String port = input.optString("port", "");
20+
String database = input.optString("database", "");
21+
String user = input.optString("user", "");
22+
String password = input.optString("password", "");
23+
query = input.optString("query", "");
24+
boolean callable = input.optBoolean("callable", false);
25+
String inParam = input.optString("inParam", "");
26+
boolean ddl = input.optBoolean("ddl", false);
2827

29-
if (isCallableQuery) {
28+
db2Service = new Db2Service(host, port, database, user, password, new ResponseMapper());
29+
db2Service.openConnection();
30+
31+
if (callable) {
3032
int queryResult = db2Service.executeCallableQuery(query, inParam);
3133
result.put("data", queryResult);
34+
} else if (ddl) {
35+
int queryResult = db2Service.applyScript(query);
36+
result.put("data", queryResult);
3237
} else {
33-
JSONArray queryResult = db2Service.executeQuery(query);
38+
org.json.JSONArray queryResult = db2Service.executeQuery(query);
3439
result.put("data", queryResult);
3540
}
36-
} catch (SQLException e) {
41+
} catch (Exception e) {
3742
JSONObject errorObj = new JSONObject();
3843
errorObj.put("message", e.getMessage());
3944
errorObj.put("stack", e.getStackTrace());
4045
errorObj.put("query", query);
41-
4246
result.put("error", errorObj);
4347
} finally {
44-
db2Service.closeConnection();
48+
if (db2Service != null) {
49+
db2Service.closeConnection();
50+
}
4551
print(result.toString());
4652
}
4753
}
4854

49-
private static String cleanStringValue(String value) {
50-
return value.replace("__PERCENT__", "%");
51-
}
52-
53-
private static String findArgument(String[] args, Argument argument) {
54-
return Arrays.stream(args)
55-
.filter(arg -> arg.startsWith(argument.getPrefix()))
56-
.map(arg -> arg.substring(argument.getStartValueIndex()))
57-
.findFirst()
58-
.orElse("");
59-
}
55+
private static String readStdin() throws IOException {
56+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
57+
String result = reader.lines().collect(Collectors.joining("\n"));
58+
return result.isEmpty() ? "{}" : result;
59+
}
60+
}
6061

6162
private static void print(String value) {
6263
System.out.println(String.format("<hackolade>%s</hackolade>", value));

shared/Db2Client/src/main/java/org/db2/Argument.java

Lines changed: 0 additions & 28 deletions
This file was deleted.

shared/Db2Client/src/main/java/org/db2/Db2Service.java

Lines changed: 105 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.json.JSONArray;
44

55
import java.sql.*;
6+
import java.util.regex.*;
67

78
public class Db2Service {
89
final String DB_URL;
@@ -30,6 +31,90 @@ public JSONArray executeQuery(String query) throws SQLException {
3031
return mapper.convertToJson(response);
3132
}
3233

34+
public int applyScript(String script) throws SQLException {
35+
String[] statements = splitStatements(script);
36+
int totalUpdateCount = 0;
37+
38+
for (String statement : statements) {
39+
statement = statement.trim();
40+
41+
if (statement.isEmpty()) {
42+
continue;
43+
}
44+
45+
Statement statementInstance = connection.createStatement();
46+
47+
try {
48+
statementInstance.execute(statement);
49+
totalUpdateCount += statementInstance.getUpdateCount();
50+
} catch (SQLException e) {
51+
int reorgPendingErrorCode = -668;
52+
53+
if (e.getErrorCode() == reorgPendingErrorCode) {
54+
String tableName = extractTableNameFromError(e.getMessage());
55+
if (tableName != null) {
56+
reorganizeTable(tableName, statementInstance);
57+
58+
// retry
59+
statementInstance.execute(statement);
60+
totalUpdateCount += statementInstance.getUpdateCount();
61+
} else {
62+
throw e;
63+
}
64+
} else {
65+
throw e;
66+
}
67+
} finally {
68+
statementInstance.close();
69+
}
70+
}
71+
72+
return totalUpdateCount;
73+
}
74+
75+
private String[] splitStatements(String query) {
76+
String[] parts = query.trim().split(";\\s+", -1);
77+
java.util.ArrayList<String> statements = new java.util.ArrayList<>();
78+
for (String part : parts) {
79+
part = part.trim();
80+
if (!part.isEmpty()) {
81+
statements.add(part);
82+
}
83+
}
84+
return statements.toArray(new String[0]);
85+
}
86+
87+
private void reorganizeTable(String tableName, Statement stmt) throws SQLException {
88+
// Use ADMIN_CMD to execute REORG TABLE command
89+
// Escape single quotes in table name for the command string
90+
String escapedTableName = tableName.replace("'", "''");
91+
String reorgSql = "CALL SYSPROC.ADMIN_CMD('REORG TABLE " + escapedTableName + "')";
92+
stmt.execute(reorgSql);
93+
if (!connection.getAutoCommit()) {
94+
connection.commit();
95+
}
96+
}
97+
98+
99+
private String extractTableNameFromError(String errorMessage) {
100+
// Extract table name from error message like: SQLERRMC=7;db1.table2
101+
Pattern pattern = Pattern.compile("SQLERRMC=\\d+;([^,;\\s]+)");
102+
Matcher matcher = pattern.matcher(errorMessage);
103+
if (matcher.find()) {
104+
String tableName = matcher.group(1).trim();
105+
// Quote the table name properly for REORG statement
106+
// If it contains a dot, split into schema.table and quote both parts
107+
if (tableName.contains(".")) {
108+
String[] parts = tableName.split("\\.", 2);
109+
if (parts.length == 2) {
110+
return "\"" + parts[0] + "\".\"" + parts[1] + "\"";
111+
}
112+
}
113+
return "\"" + tableName + "\"";
114+
}
115+
return null;
116+
}
117+
33118
public int executeCallableQuery(String query, String inParam) throws SQLException {
34119
this.callableStatement = connection.prepareCall(query);
35120

@@ -53,29 +138,33 @@ public void openConnection() throws SQLException {
53138
}
54139

55140
public void closeConnection() {
56-
if (response != null) {
141+
if (this.response != null) {
57142
try {
58-
response.close();
59-
} catch (SQLException e) {
60-
/* Ignored */}
143+
this.response.close();
144+
} catch (SQLException _) {
145+
/* Ignored */
146+
}
61147
}
62-
if (statement != null) {
148+
if (this.statement != null) {
63149
try {
64-
statement.close();
65-
} catch (SQLException e) {
66-
/* Ignored */}
150+
this.statement.close();
151+
} catch (SQLException _) {
152+
/* Ignored */
153+
}
67154
}
68-
if (callableStatement != null) {
155+
if (this.callableStatement != null) {
69156
try {
70-
callableStatement.close();
71-
} catch (SQLException e) {
72-
/* Ignored */}
157+
this.callableStatement.close();
158+
} catch (SQLException _) {
159+
/* Ignored */
160+
}
73161
}
74-
if (connection != null) {
162+
if (this.connection != null) {
75163
try {
76-
connection.close();
77-
} catch (SQLException e) {
78-
/* Ignored */}
164+
this.connection.close();
165+
} catch (SQLException _) {
166+
/* Ignored */
167+
}
79168
}
80169
}
81170

shared/addons/Db2Client.jar

366 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)