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, };