From 51b3a69d5d670b8125f9015dca29f5dbc4dd1d97 Mon Sep 17 00:00:00 2001 From: Sergey Konovalov Date: Thu, 16 Oct 2025 15:35:52 +0300 Subject: [PATCH] [bug] Fix sql for oracle NCLOB type; Fix bug 77666 --- .../databaseConnectors/baseConnector.js | 15 +++++++- .../databaseConnectors/oracleConnector.js | 13 ++++++- DocService/sources/taskresult.js | 7 ++-- .../databaseTests/baseConnector.tests.js | 36 ++++++++++++------- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/DocService/sources/databaseConnectors/baseConnector.js b/DocService/sources/databaseConnectors/baseConnector.js index d6d5cd30..5d572f7a 100644 --- a/DocService/sources/databaseConnectors/baseConnector.js +++ b/DocService/sources/databaseConnectors/baseConnector.js @@ -439,6 +439,17 @@ function getTableColumns(ctx, tableName) { }); } +/** + * Generate SQL condition to check if a field is not empty (fallback implementation) + * Database-specific connectors can override this function + * @param {string} fieldName - Name of the field to check + * @returns {string} SQL condition string + */ +function getNotEmptyConditionFallback(fieldName) { + // Default implementation for most databases: check both NULL and empty string + return `${fieldName} IS NOT NULL AND ${fieldName} != ''`; +} + module.exports = { insertChangesPromise, deleteChangesPromise, @@ -454,5 +465,7 @@ module.exports = { getTableColumns, getDateTime: _getDateTime2, ...connectorUtilities, - ...dbInstance + ...dbInstance, + // Use connector-specific implementation if available, otherwise use fallback + getNotEmptyCondition: dbInstance.getNotEmptyCondition || getNotEmptyConditionFallback }; diff --git a/DocService/sources/databaseConnectors/oracleConnector.js b/DocService/sources/databaseConnectors/oracleConnector.js index 5381294e..e08fa238 100644 --- a/DocService/sources/databaseConnectors/oracleConnector.js +++ b/DocService/sources/databaseConnectors/oracleConnector.js @@ -445,6 +445,16 @@ async function insertChangesAsync(ctx, tableChanges, startIndex, objChanges, doc return result; } +/** + * Generate SQL condition to check if a field is not empty + * Oracle-specific: NCLOB cannot be compared with != operator, and empty strings are NULL + * @param {string} fieldName - Name of the field to check + * @returns {string} SQL condition string + */ +function getNotEmptyCondition(fieldName) { + return `${fieldName} IS NOT NULL`; +} + module.exports = { sqlQuery, closePool, @@ -456,5 +466,6 @@ module.exports = { getDocumentsWithChanges, getExpired, upsert, - insertChanges + insertChanges, + getNotEmptyCondition }; diff --git a/DocService/sources/taskresult.js b/DocService/sources/taskresult.js index 74697ded..83159bcd 100644 --- a/DocService/sources/taskresult.js +++ b/DocService/sources/taskresult.js @@ -135,7 +135,7 @@ function select(ctx, docId) { }); } /** - * Convert task object to SQL update/condition array + * Generate SQL SET/WHERE clauses from task object * @param {TaskResultData} task - Task data object * @param {boolean} updateTime - Whether to update last_open_date * @param {boolean} isMask - Whether this is for WHERE clause (mask mode) @@ -145,7 +145,7 @@ function select(ctx, docId) { * * Special mask values: * - Use 'NOT_EMPTY' as field value in mask mode to check for non-empty callback - * - Example: {callback: 'NOT_EMPTY'} generates "callback IS NOT NULL AND callback != ''" + * - Uses baseConnector.getNotEmptyCondition() for database-specific SQL generation */ function toUpdateArray(task, updateTime, isMask, values, setPassword) { const res = []; @@ -177,7 +177,8 @@ function toUpdateArray(task, updateTime, isMask, values, setPassword) { } // Add callback non-empty check for mask if (isMask && task.callback === 'NOT_EMPTY') { - res.push(`callback IS NOT NULL AND callback != ''`); + // Use database-specific condition (Oracle NCLOB needs special handling) + res.push(sqlBase.getNotEmptyCondition('callback')); } if (null != task.baseurl) { const sqlParam = addSqlParam(task.baseurl, values); diff --git a/tests/integration/databaseTests/baseConnector.tests.js b/tests/integration/databaseTests/baseConnector.tests.js index 3319cf37..11f5923b 100644 --- a/tests/integration/databaseTests/baseConnector.tests.js +++ b/tests/integration/databaseTests/baseConnector.tests.js @@ -210,7 +210,14 @@ afterAll(async () => { const updateIfIds = Object.values(updateIfCases); const tableChangesIds = [...emptyCallbacksCase, ...documentsWithChangesCase, ...changesIds, ...insertIds]; - const tableResultIds = [...emptyCallbacksCase, ...documentsWithChangesCase, ...getExpiredCase, ...getCountWithStatusCase, ...upsertIds, ...updateIfIds]; + const tableResultIds = [ + ...emptyCallbacksCase, + ...documentsWithChangesCase, + ...getExpiredCase, + ...getCountWithStatusCase, + ...upsertIds, + ...updateIfIds + ]; const deletionPool = [ deleteRowsByIds(cfgTableChanges, tableChangesIds), @@ -292,17 +299,23 @@ describe('Base database connector', () => { describe('Add changes', () => { for (const testCase in insertCases) { - test(`${testCase} rows inserted`, async () => { - const docId = insertCases[testCase]; - const objChanges = createChanges(+testCase, date); + // Increase timeout for large inserts (5000+ rows can take longer on some databases) + const timeout = +testCase >= 5000 ? 15000 : 5000; + test( + `${testCase} rows inserted`, + async () => { + const docId = insertCases[testCase]; + const objChanges = createChanges(+testCase, date); - await noRowsExistenceCheck(cfgTableChanges, docId); + await noRowsExistenceCheck(cfgTableChanges, docId); - await baseConnector.insertChangesPromise(ctx, objChanges, docId, index, user); - const result = await getRowsCountById(cfgTableChanges, docId); + await baseConnector.insertChangesPromise(ctx, objChanges, docId, index, user); + const result = await getRowsCountById(cfgTableChanges, docId); - expect(result).toEqual(objChanges.length); - }); + expect(result).toEqual(objChanges.length); + }, + timeout + ); } }); @@ -482,10 +495,7 @@ describe('Base database connector', () => { const taskEmptyCallback = createTask(updateIfCases.emptyCallback, ''); // Insert two rows: one with callback, one without - await Promise.all([ - insertIntoResultTable(date, taskWithCallback), - insertIntoResultTable(date, taskEmptyCallback) - ]); + await Promise.all([insertIntoResultTable(date, taskWithCallback), insertIntoResultTable(date, taskEmptyCallback)]); // Update mask: only update rows with non-empty callback and status=None const mask = new taskResult.TaskResultData();