diff --git a/forward_engineering/api.js b/forward_engineering/api.js
index 178d5d7..20f1620 100644
--- a/forward_engineering/api.js
+++ b/forward_engineering/api.js
@@ -1,5 +1,7 @@
const { generateContainerScript } = require('./api/generateContainerScript');
const { isDropInStatements } = require('./api/isDropInStatements');
+const { testConnection } = require('../shared/api/testConnection');
+const { applyToInstance } = require('./api/applyToInstance');
module.exports = {
generateScript(data, logger, callback, app) {
@@ -16,13 +18,9 @@ module.exports = {
throw new Error('Not implemented');
},
- applyToInstance(connectionInfo, logger, callback, app) {
- throw new Error('Not implemented');
- },
+ applyToInstance,
- testConnection(connectionInfo, logger, callback, app) {
- throw new Error('Not implemented');
- },
+ testConnection,
isDropInStatements,
};
diff --git a/forward_engineering/api/applyToInstance.js b/forward_engineering/api/applyToInstance.js
new file mode 100644
index 0000000..1df0c53
--- /dev/null
+++ b/forward_engineering/api/applyToInstance.js
@@ -0,0 +1,23 @@
+const { logHelper } = require('../../shared/helpers/logHelper');
+const { connectionHelper } = require('../../shared/helpers/connectionHelper');
+const { instanceHelper } = require('../../shared/helpers/instanceHelper');
+
+async function applyToInstance(connectionInfo, logger, callback, app) {
+ const applyToInstanceLogger = logHelper.createLogger({
+ title: 'Apply to instance',
+ hiddenKeys: connectionInfo.hiddenKeys,
+ logger,
+ });
+
+ try {
+ const connection = await connectionHelper.connect({ connectionInfo, logger: applyToInstanceLogger });
+ await instanceHelper.executeQuery({ connection, query: connectionInfo.script, ddl: true });
+
+ callback();
+ } catch (err) {
+ applyToInstanceLogger.error(err);
+ callback(err);
+ }
+}
+
+module.exports = { applyToInstance };
diff --git a/forward_engineering/config.json b/forward_engineering/config.json
index e213887..aabbeec 100644
--- a/forward_engineering/config.json
+++ b/forward_engineering/config.json
@@ -9,7 +9,7 @@
}
],
"hasUpdateScript": false,
- "applyScriptToInstance": false,
+ "applyScriptToInstance": true,
"combinedContainers": true,
"feLevelSelector": {
"container": true,
diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js
index cfa704e..003f62d 100644
--- a/reverse_engineering/api.js
+++ b/reverse_engineering/api.js
@@ -13,6 +13,7 @@ const { instanceHelper } = require('../shared/helpers/instanceHelper');
const { logHelper } = require('../shared/helpers/logHelper');
const { TABLE_TYPE } = require('../constants/constants');
const { nameHelper } = require('../shared/helpers/nameHelper');
+const { testConnection } = require('../shared/api/testConnection');
/**
* @param {ConnectionInfo} connectionInfo
@@ -35,34 +36,6 @@ const disconnect = async (connectionInfo, appLogger, callback) => {
}
};
-/**
- * @param {ConnectionInfo} connectionInfo
- * @param {AppLogger} appLogger
- * @param {Callback} callback
- * @param {App} app
- */
-const testConnection = async (connectionInfo, appLogger, callback, app) => {
- const logger = logHelper.createLogger({
- title: 'Test database connection',
- hiddenKeys: connectionInfo.hiddenKeys,
- logger: appLogger,
- });
-
- try {
- logger.info(connectionInfo);
-
- const connection = await connectionHelper.connect({ connectionInfo, logger });
- const version = await instanceHelper.getDbVersion({ connection });
- await connectionHelper.disconnect();
-
- logger.info('Db version: ' + version);
- callback();
- } catch (error) {
- logger.error(error);
- callback(error);
- }
-};
-
/**
* @param {ConnectionInfo} connectionInfo
* @param {AppLogger} appLogger
diff --git a/shared/Db2Client/src/main/java/org/db2/App.java b/shared/Db2Client/src/main/java/org/db2/App.java
index b08abfc..c230f97 100644
--- a/shared/Db2Client/src/main/java/org/db2/App.java
+++ b/shared/Db2Client/src/main/java/org/db2/App.java
@@ -1,62 +1,63 @@
package org.db2;
-import org.json.JSONArray;
import org.json.JSONObject;
-import java.sql.SQLException;
-import java.util.Arrays;
+import java.io.*;
+import java.util.stream.Collectors;
public class App {
public static void main(String[] args) {
- String host = findArgument(args, Argument.HOST);
- String port = findArgument(args, Argument.PORT);
- String database = findArgument(args, Argument.DATABASE);
- String user = findArgument(args, Argument.USER);
- String password = findArgument(args, Argument.PASSWORD);
- String query = cleanStringValue(findArgument(args, Argument.QUERY));
- String callable = findArgument(args, Argument.CALLABLE);
- String inParam = findArgument(args, Argument.IN_PARAM);
-
- Db2Service db2Service = new Db2Service(host, port, database, user, password, new ResponseMapper());
-
JSONObject result = new JSONObject();
+ String query = "";
+ Db2Service db2Service = null;
try {
- db2Service.openConnection();
+ String jsonInput = readStdin();
+ JSONObject input = new JSONObject(jsonInput);
- boolean isCallableQuery = Boolean.parseBoolean(callable);
+ String host = input.optString("host", "");
+ String port = input.optString("port", "");
+ String database = input.optString("database", "");
+ String user = input.optString("user", "");
+ String password = input.optString("password", "");
+ query = input.optString("query", "");
+ boolean callable = input.optBoolean("callable", false);
+ String inParam = input.optString("inParam", "");
+ boolean ddl = input.optBoolean("ddl", false);
- if (isCallableQuery) {
+ db2Service = new Db2Service(host, port, database, user, password, new ResponseMapper());
+ db2Service.openConnection();
+
+ if (callable) {
int queryResult = db2Service.executeCallableQuery(query, inParam);
result.put("data", queryResult);
+ } else if (ddl) {
+ int queryResult = db2Service.applyScript(query);
+ result.put("data", queryResult);
} else {
- JSONArray queryResult = db2Service.executeQuery(query);
+ org.json.JSONArray queryResult = db2Service.executeQuery(query);
result.put("data", queryResult);
}
- } catch (SQLException e) {
+ } catch (Exception e) {
JSONObject errorObj = new JSONObject();
errorObj.put("message", e.getMessage());
errorObj.put("stack", e.getStackTrace());
errorObj.put("query", query);
-
result.put("error", errorObj);
} finally {
- db2Service.closeConnection();
+ if (db2Service != null) {
+ db2Service.closeConnection();
+ }
print(result.toString());
}
}
- private static String cleanStringValue(String value) {
- return value.replace("__PERCENT__", "%");
- }
-
- private static String findArgument(String[] args, Argument argument) {
- return Arrays.stream(args)
- .filter(arg -> arg.startsWith(argument.getPrefix()))
- .map(arg -> arg.substring(argument.getStartValueIndex()))
- .findFirst()
- .orElse("");
- }
+ private static String readStdin() throws IOException {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {
+ String result = reader.lines().collect(Collectors.joining("\n"));
+ return result.isEmpty() ? "{}" : result;
+ }
+ }
private static void print(String value) {
System.out.println(String.format("%s", value));
diff --git a/shared/Db2Client/src/main/java/org/db2/Argument.java b/shared/Db2Client/src/main/java/org/db2/Argument.java
deleted file mode 100644
index 1e72dd7..0000000
--- a/shared/Db2Client/src/main/java/org/db2/Argument.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.db2;
-
-public enum Argument {
- HOST("--host", 7),
- USER("--user", 7),
- PASSWORD("--pass", 7),
- PORT("--port", 7),
- QUERY("--query", 8),
- DATABASE("--database", 11),
- CALLABLE("--callable", 11),
- IN_PARAM("--inparam", 10);
-
- private final String argPrefix;
- private final int startValueIndex;
-
- Argument(String argPrefix, int startValueIndex) {
- this.argPrefix = argPrefix;
- this.startValueIndex = startValueIndex;
- }
-
- public String getPrefix() {
- return this.argPrefix;
- }
-
- public int getStartValueIndex() {
- return this.startValueIndex;
- }
-}
diff --git a/shared/Db2Client/src/main/java/org/db2/Db2Service.java b/shared/Db2Client/src/main/java/org/db2/Db2Service.java
index 94ab1cd..21778aa 100644
--- a/shared/Db2Client/src/main/java/org/db2/Db2Service.java
+++ b/shared/Db2Client/src/main/java/org/db2/Db2Service.java
@@ -3,6 +3,7 @@
import org.json.JSONArray;
import java.sql.*;
+import java.util.regex.*;
public class Db2Service {
final String DB_URL;
@@ -30,6 +31,90 @@ public JSONArray executeQuery(String query) throws SQLException {
return mapper.convertToJson(response);
}
+ public int applyScript(String script) throws SQLException {
+ String[] statements = splitStatements(script);
+ int totalUpdateCount = 0;
+
+ for (String statement : statements) {
+ statement = statement.trim();
+
+ if (statement.isEmpty()) {
+ continue;
+ }
+
+ Statement statementInstance = connection.createStatement();
+
+ try {
+ statementInstance.execute(statement);
+ totalUpdateCount += statementInstance.getUpdateCount();
+ } catch (SQLException e) {
+ int reorgPendingErrorCode = -668;
+
+ if (e.getErrorCode() == reorgPendingErrorCode) {
+ String tableName = extractTableNameFromError(e.getMessage());
+ if (tableName != null) {
+ reorganizeTable(tableName, statementInstance);
+
+ // retry
+ statementInstance.execute(statement);
+ totalUpdateCount += statementInstance.getUpdateCount();
+ } else {
+ throw e;
+ }
+ } else {
+ throw e;
+ }
+ } finally {
+ statementInstance.close();
+ }
+ }
+
+ return totalUpdateCount;
+ }
+
+ private String[] splitStatements(String query) {
+ String[] parts = query.trim().split(";\\s+", -1);
+ java.util.ArrayList statements = new java.util.ArrayList<>();
+ for (String part : parts) {
+ part = part.trim();
+ if (!part.isEmpty()) {
+ statements.add(part);
+ }
+ }
+ return statements.toArray(new String[0]);
+ }
+
+ private void reorganizeTable(String tableName, Statement stmt) throws SQLException {
+ // Use ADMIN_CMD to execute REORG TABLE command
+ // Escape single quotes in table name for the command string
+ String escapedTableName = tableName.replace("'", "''");
+ String reorgSql = "CALL SYSPROC.ADMIN_CMD('REORG TABLE " + escapedTableName + "')";
+ stmt.execute(reorgSql);
+ if (!connection.getAutoCommit()) {
+ connection.commit();
+ }
+ }
+
+
+ private String extractTableNameFromError(String errorMessage) {
+ // Extract table name from error message like: SQLERRMC=7;db1.table2
+ Pattern pattern = Pattern.compile("SQLERRMC=\\d+;([^,;\\s]+)");
+ Matcher matcher = pattern.matcher(errorMessage);
+ if (matcher.find()) {
+ String tableName = matcher.group(1).trim();
+ // Quote the table name properly for REORG statement
+ // If it contains a dot, split into schema.table and quote both parts
+ if (tableName.contains(".")) {
+ String[] parts = tableName.split("\\.", 2);
+ if (parts.length == 2) {
+ return "\"" + parts[0] + "\".\"" + parts[1] + "\"";
+ }
+ }
+ return "\"" + tableName + "\"";
+ }
+ return null;
+ }
+
public int executeCallableQuery(String query, String inParam) throws SQLException {
this.callableStatement = connection.prepareCall(query);
@@ -53,29 +138,33 @@ public void openConnection() throws SQLException {
}
public void closeConnection() {
- if (response != null) {
+ if (this.response != null) {
try {
- response.close();
- } catch (SQLException e) {
- /* Ignored */}
+ this.response.close();
+ } catch (SQLException _) {
+ /* Ignored */
+ }
}
- if (statement != null) {
+ if (this.statement != null) {
try {
- statement.close();
- } catch (SQLException e) {
- /* Ignored */}
+ this.statement.close();
+ } catch (SQLException _) {
+ /* Ignored */
+ }
}
- if (callableStatement != null) {
+ if (this.callableStatement != null) {
try {
- callableStatement.close();
- } catch (SQLException e) {
- /* Ignored */}
+ this.callableStatement.close();
+ } catch (SQLException _) {
+ /* Ignored */
+ }
}
- if (connection != null) {
+ if (this.connection != null) {
try {
- connection.close();
- } catch (SQLException e) {
- /* Ignored */}
+ this.connection.close();
+ } catch (SQLException _) {
+ /* Ignored */
+ }
}
}
diff --git a/shared/addons/Db2Client.jar b/shared/addons/Db2Client.jar
index 55670cb..364efc0 100644
Binary files a/shared/addons/Db2Client.jar and b/shared/addons/Db2Client.jar differ
diff --git a/shared/api/testConnection.js b/shared/api/testConnection.js
new file mode 100644
index 0000000..b8c4d64
--- /dev/null
+++ b/shared/api/testConnection.js
@@ -0,0 +1,31 @@
+const { logHelper } = require('../helpers/logHelper');
+const { connectionHelper } = require('../helpers/connectionHelper');
+const { instanceHelper } = require('../helpers/instanceHelper');
+
+/**
+ * @param {ConnectionInfo} connectionInfo
+ * @param {AppLogger} appLogger
+ * @param {Callback} callback
+ * @param {App} app
+ */
+const testConnection = async (connectionInfo, appLogger, callback, app) => {
+ const logger = logHelper.createLogger({
+ title: 'Test database connection',
+ hiddenKeys: connectionInfo.hiddenKeys,
+ logger: appLogger,
+ });
+
+ try {
+ const connection = await connectionHelper.connect({ connectionInfo, logger });
+ const version = await instanceHelper.getDbVersion({ connection });
+ await connectionHelper.disconnect();
+
+ logger.info('Db version: ' + version);
+ callback();
+ } catch (error) {
+ logger.error(error);
+ callback(error);
+ }
+};
+
+module.exports = { testConnection };
diff --git a/shared/helpers/connectionHelper.js b/shared/helpers/connectionHelper.js
index e6f6d3b..8d8e013 100644
--- a/shared/helpers/connectionHelper.js
+++ b/shared/helpers/connectionHelper.js
@@ -22,36 +22,11 @@ let connection;
const isWindows = () => os.platform() === 'win32';
/**
- * @param {string} argKey
- * @param {string | number} argValue
- * @returns {string}
- */
-const createArgument = (argKey, argValue) => ` --${argKey}="${argValue}"`;
-
-/**
- * @param {{ [argKey: string]: string }} queryData
+ * @param {{ clientPath: string }}
* @returns {string[]}
*/
-const getQueryArguments = queryData => {
- return Object.entries(queryData).reduce((result, [argKey, argValue]) => {
- return [...result, createArgument(argKey, argValue)];
- }, []);
-};
-
-/**
- * @param {{ clientPath: string, connectionInfo: ConnectionInfo }}
- * @returns {string[]}
- */
-const buildCommand = ({ clientPath, connectionInfo }) => {
- let commandArgs = ['-jar', clientPath];
-
- connectionInfo.host && commandArgs.push(createArgument('host', connectionInfo.host));
- connectionInfo.port && commandArgs.push(createArgument('port', connectionInfo.port));
- connectionInfo.database && commandArgs.push(createArgument('database', connectionInfo.database));
- connectionInfo.userName && commandArgs.push(createArgument('user', connectionInfo.userName));
- connectionInfo.userPassword && commandArgs.push(createArgument('pass', connectionInfo.userPassword));
-
- return commandArgs;
+const buildCommand = ({ clientPath }) => {
+ return ['-jar', clientPath];
};
/**
@@ -89,14 +64,14 @@ const createConnection = async ({ connectionInfo, logger }) => {
// If you need to change this clientPath, please ensure that your changes work in the packaged plugin
const clientPath = path.resolve(__dirname, '..', 'addons', 'Db2Client.jar');
- const clientCommandArguments = buildCommand({ clientPath, connectionInfo });
+ const clientCommandArguments = buildCommand({ clientPath });
return {
execute: queryData => {
return new Promise((resolve, reject) => {
- const queryArguments = getQueryArguments(queryData);
- const queryResult = spawn(`"${javaPath}"`, [...clientCommandArguments, ...queryArguments], {
+ const queryResult = spawn(`"${javaPath}"`, clientCommandArguments, {
shell: true,
+ stdio: 'pipe',
});
queryResult.on('error', error => {
@@ -113,6 +88,25 @@ const createConnection = async ({ connectionInfo, logger }) => {
resultData.push(data);
});
+ const inputJson = JSON.stringify({
+ host: connectionInfo.host || '',
+ port: connectionInfo.port || '',
+ database: connectionInfo.database || '',
+ user: connectionInfo.userName || '',
+ password: connectionInfo.userPassword || '',
+ query: queryData.query || '',
+ callable: queryData.callable || false,
+ inParam: queryData.inparam ? String(queryData.inparam) : '',
+ ddl: queryData.ddl || false,
+ });
+
+ queryResult.stdin.on('error', error => {
+ reject(error);
+ });
+
+ queryResult.stdin.write(inputJson, 'utf8');
+ queryResult.stdin.end();
+
queryResult.on('close', code => {
if (code !== 0) {
reject(new Error(Buffer.concat(errorData).toString()));
diff --git a/shared/helpers/instanceHelper.js b/shared/helpers/instanceHelper.js
index 0b89a27..995b8db 100644
--- a/shared/helpers/instanceHelper.js
+++ b/shared/helpers/instanceHelper.js
@@ -91,12 +91,19 @@ const getTableDdl = async ({ connection, schemaName, tableName, tableType, logge
}
};
+/**
+ * @param {{ connection: Connection, query: string, ddl?: boolean }}
+ * @returns {Promise}
+ */
+const executeQuery = async ({ connection, query, ddl = false }) => await connection.execute({ query, ddl });
+
const instanceHelper = {
getDbVersion,
getSchemaNames,
getSchemaProperties,
getDatabasesWithTableNames,
getTableDdl,
+ executeQuery,
};
module.exports = {
diff --git a/shared/types.d.ts b/shared/types.d.ts
index c593458..cdf87c1 100644
--- a/shared/types.d.ts
+++ b/shared/types.d.ts
@@ -5,113 +5,123 @@ type FilePath = string;
type AppTarget = 'Db2';
type App = {
- require: (packageName: string) => any;
+ require: (packageName: string) => any;
};
type AppLogger = {
- log: (logType: string, logData: { message: string }, title: string, hiddenKeys: string[]) => void;
+ log: (logType: string, logData: { message: string }, title: string, hiddenKeys: string[]) => void;
};
type Pagination = {
- enabled: boolean;
- value: number;
+ enabled: boolean;
+ value: number;
};
type RecordSamplingType = 'relative' | 'absolute';
type RecordSamplingSettings = {
- [key: RecordSamplingType]: {
- value: number;
- };
- active: RecordSamplingType;
- maxValue: number;
+ [key: RecordSamplingType]: {
+ value: number;
+ };
+ active: RecordSamplingType;
+ maxValue: number;
};
enum AuthTypeEnum {
- usernamePassword = 'username_password',
+ usernamePassword = 'username_password',
}
type AuthType = `${AuthTypeEnum}`;
type ConnectionInfo = {
- name: string;
- host: string;
- authType: AuthType;
- port: number;
- userName: string;
- userPassword: string;
- database: string;
- target: AppTarget;
- id: UUID;
- appVersion: string;
- tempFolder: FilePath;
- pluginVersion?: string;
- includeSystemCollection: boolean;
- includeEmptyCollection: boolean;
- pagination: Pagination;
- recordSamplingSettings: RecordSamplingSettings;
- queryRequestTimeout: number;
- applyToInstanceQueryRequestTimeout: number;
- activeProxyPool: string[];
- hiddenKeys: string[];
- options: any;
+ name: string;
+ host: string;
+ authType: AuthType;
+ port: number;
+ userName: string;
+ userPassword: string;
+ database: string;
+ target: AppTarget;
+ id: UUID;
+ appVersion: string;
+ tempFolder: FilePath;
+ pluginVersion?: string;
+ includeSystemCollection: boolean;
+ includeEmptyCollection: boolean;
+ pagination: Pagination;
+ recordSamplingSettings: RecordSamplingSettings;
+ queryRequestTimeout: number;
+ applyToInstanceQueryRequestTimeout: number;
+ activeProxyPool: string[];
+ hiddenKeys: string[];
+ options: any;
};
type Logger = {
- error: (error: Error) => void;
- info: (message: string) => void;
- progress: (message: string, containerName: string, entityName: string) => void;
+ error: (error: Error) => void;
+ info: (message: string) => void;
+ progress: (message: string, containerName: string, entityName: string) => void;
};
type Callback = (error: Error, result: any[], info?: { version?: string }, relationships?: any[]) => void;
type NameMap = {
- [key: string]: NameMap | string[];
+ [key: string]: NameMap | string[];
};
type BucketCollectionNamesData = {
- dbName: string;
- scopeName?: string;
- dbCollections?: string[];
- status?: string;
- disabledTooltip?: string;
+ dbName: string;
+ scopeName?: string;
+ dbCollections?: string[];
+ status?: string;
+ disabledTooltip?: string;
};
type Document = {
- [key: string]: any;
+ [key: string]: any;
};
type DbCollectionData = {
- dbName: string;
- collectionName: string;
- documentKind: string;
- standardDoc: object;
- collectionDocs: object;
- bucketInfo: object;
- emptyBucket: boolean;
- indexes: object[];
- documents: Document[];
- entityLevel: object;
+ dbName: string;
+ collectionName: string;
+ documentKind: string;
+ standardDoc: object;
+ collectionDocs: object;
+ bucketInfo: object;
+ emptyBucket: boolean;
+ indexes: object[];
+ documents: Document[];
+ entityLevel: object;
};
type Connection = {
- execute: ({ query, callable, inparam }: { query: string, callable?: boolean, inparam?: number }) => Promise;
+ execute: ({
+ query,
+ callable,
+ inparam,
+ ddl,
+ }: {
+ query: string;
+ callable?: boolean;
+ inparam?: number;
+ ddl?: boolean;
+ }) => Promise;
};
export {
- App,
- AppLogger,
- AppTarget,
- BucketCollectionNamesData,
- Callback,
- Connection,
- ConnectionInfo,
- DbCollectionData,
- Document,
- FilePath,
- NameMap,
- Logger,
- Pagination,
- RecordSamplingSettings,
- UUID,
+ App,
+ AppLogger,
+ AppTarget,
+ BucketCollectionNamesData,
+ Callback,
+ Connection,
+ ConnectionInfo,
+ DbCollectionData,
+ Document,
+ FilePath,
+ NameMap,
+ Logger,
+ Pagination,
+ RecordSamplingSettings,
+ UUID,
};