mirror of
https://github.com/ONLYOFFICE/server.git
synced 2026-02-10 18:05:07 +08:00
[bug] Normalize null to empty string for NCLOB/CLOB columns; Fix bug 77676
This commit is contained in:
@ -75,14 +75,60 @@ let pool = null;
|
|||||||
oracledb.fetchAsString = [oracledb.NCLOB, oracledb.CLOB];
|
oracledb.fetchAsString = [oracledb.NCLOB, oracledb.CLOB];
|
||||||
oracledb.autoCommit = true;
|
oracledb.autoCommit = true;
|
||||||
|
|
||||||
function columnsToLowercase(rows) {
|
/**
|
||||||
|
* WeakMap cache for column type maps
|
||||||
|
* Key: metaData array reference from Oracle result
|
||||||
|
* Value: Object mapping column names (lowercase) to boolean (is NCLOB/CLOB)
|
||||||
|
* Automatically garbage collected when metaData is no longer referenced
|
||||||
|
*/
|
||||||
|
const columnTypeMapCache = new WeakMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or build column type map from metadata
|
||||||
|
* @param {Array} metaData - Column metadata from Oracle
|
||||||
|
* @returns {Object.<string, boolean>} Map of column name (lowercase) to isClobColumn flag
|
||||||
|
*/
|
||||||
|
function getColumnTypeMap(metaData) {
|
||||||
|
let columnTypeMap = columnTypeMapCache.get(metaData);
|
||||||
|
|
||||||
|
if (!columnTypeMap) {
|
||||||
|
columnTypeMap = {};
|
||||||
|
for (let i = 0; i < metaData.length; i++) {
|
||||||
|
const col = metaData[i];
|
||||||
|
// Check if column is NCLOB/CLOB (converted to string by fetchAsString config)
|
||||||
|
const isClobColumn = col.dbType === oracledb.DB_TYPE_NCLOB || col.dbType === oracledb.DB_TYPE_CLOB;
|
||||||
|
columnTypeMap[col.name.toLowerCase()] = isClobColumn;
|
||||||
|
}
|
||||||
|
columnTypeMapCache.set(metaData, columnTypeMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return columnTypeMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert column names to lowercase and normalize null values
|
||||||
|
* Oracle returns null for empty NCLOB/CLOB fields, converts to empty string for consistency with other databases
|
||||||
|
* @param {Array<Object>} rows - Query result rows
|
||||||
|
* @param {Array} metaData - Column metadata from Oracle (optional)
|
||||||
|
* @returns {Array<Object>} Formatted rows with lowercase column names and normalized values
|
||||||
|
*/
|
||||||
|
function columnsToLowercase(rows, metaData) {
|
||||||
|
const columnTypeMap = metaData ? getColumnTypeMap(metaData) : null;
|
||||||
|
|
||||||
const formattedRows = [];
|
const formattedRows = [];
|
||||||
for (const row of rows) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const row = rows[i];
|
||||||
const newRow = {};
|
const newRow = {};
|
||||||
for (const column in row) {
|
for (const column in row) {
|
||||||
if (Object.hasOwn(row, column)) {
|
if (!Object.hasOwn(row, column)) continue;
|
||||||
newRow[column.toLowerCase()] = row[column];
|
|
||||||
|
const columnLower = column.toLowerCase();
|
||||||
|
let value = row[column];
|
||||||
|
// Normalize null to empty string for NCLOB/CLOB columns
|
||||||
|
if (value === null && columnTypeMap?.[columnLower]) {
|
||||||
|
value = '';
|
||||||
}
|
}
|
||||||
|
newRow[columnLower] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
formattedRows.push(newRow);
|
formattedRows.push(newRow);
|
||||||
@ -121,7 +167,7 @@ async function executeQuery(ctx, sqlCommand, values = [], noModifyRes = false, n
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result?.rows) {
|
if (result?.rows) {
|
||||||
output = columnsToLowercase(result.rows);
|
output = columnsToLowercase(result.rows, result.metaData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
output = result;
|
output = result;
|
||||||
@ -204,6 +250,7 @@ async function executeBunch(ctx, sqlCommand, values = [], options, noLog = false
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closePool() {
|
function closePool() {
|
||||||
|
// WeakMap cache is automatically garbage collected, no manual cleanup needed
|
||||||
return pool?.close(forceClosingCountdownMs);
|
return pool?.close(forceClosingCountdownMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -104,6 +104,7 @@ const updateIfCases = {
|
|||||||
notEmpty: 'baseConnector-updateIf()-tester-not-empty-callback',
|
notEmpty: 'baseConnector-updateIf()-tester-not-empty-callback',
|
||||||
emptyCallback: 'baseConnector-updateIf()-tester-empty-callback'
|
emptyCallback: 'baseConnector-updateIf()-tester-empty-callback'
|
||||||
};
|
};
|
||||||
|
const oracleNullHandlingCases = ['baseConnector-oracle-nclob-null-handling', 'baseConnector-oracle-nclob-null-handling-2'];
|
||||||
|
|
||||||
function createChanges(changesLength, date) {
|
function createChanges(changesLength, date) {
|
||||||
const objChanges = [
|
const objChanges = [
|
||||||
@ -216,7 +217,8 @@ afterAll(async () => {
|
|||||||
...getExpiredCase,
|
...getExpiredCase,
|
||||||
...getCountWithStatusCase,
|
...getCountWithStatusCase,
|
||||||
...upsertIds,
|
...upsertIds,
|
||||||
...updateIfIds
|
...updateIfIds,
|
||||||
|
...oracleNullHandlingCases
|
||||||
];
|
];
|
||||||
|
|
||||||
const deletionPool = [
|
const deletionPool = [
|
||||||
@ -488,6 +490,47 @@ describe('Base database connector', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Oracle NCLOB null handling', () => {
|
||||||
|
const nullHandlingCase = 'baseConnector-oracle-nclob-null-handling';
|
||||||
|
|
||||||
|
test('Empty callback is retrieved as empty string (not null)', async () => {
|
||||||
|
const date = new Date();
|
||||||
|
const task = createTask(nullHandlingCase, ''); // Empty callback
|
||||||
|
|
||||||
|
await noRowsExistenceCheck(cfgTableResult, task.key);
|
||||||
|
await insertIntoResultTable(date, task);
|
||||||
|
|
||||||
|
// Retrieve the row and check callback field
|
||||||
|
const result = await executeSql(`SELECT callback, baseurl FROM ${cfgTableResult} WHERE id = '${task.key}';`);
|
||||||
|
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
// Oracle should normalize null NCLOB to empty string
|
||||||
|
expect(result[0].callback).toEqual('');
|
||||||
|
expect(result[0].baseurl).toEqual('');
|
||||||
|
// Verify they are strings, not null
|
||||||
|
expect(typeof result[0].callback).toEqual('string');
|
||||||
|
expect(typeof result[0].baseurl).toEqual('string');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Null callback does not cause TypeError in getCallbackByUserIndex', async () => {
|
||||||
|
const date = new Date();
|
||||||
|
const task = createTask(nullHandlingCase + '-2', '');
|
||||||
|
|
||||||
|
await insertIntoResultTable(date, task);
|
||||||
|
|
||||||
|
const result = await executeSql(`SELECT callback FROM ${cfgTableResult} WHERE id = '${task.key}';`);
|
||||||
|
|
||||||
|
// This should not throw TypeError
|
||||||
|
const userCallback = new baseConnector.UserCallback();
|
||||||
|
expect(() => {
|
||||||
|
userCallback.getCallbackByUserIndex(ctx, result[0].callback, 1);
|
||||||
|
}).not.toThrow();
|
||||||
|
|
||||||
|
const callbackResult = userCallback.getCallbackByUserIndex(ctx, result[0].callback, 1);
|
||||||
|
expect(callbackResult).toEqual('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('updateIf() method', () => {
|
describe('updateIf() method', () => {
|
||||||
test('Update with NOT_EMPTY callback mask', async () => {
|
test('Update with NOT_EMPTY callback mask', async () => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|||||||
Reference in New Issue
Block a user