diff --git a/packages/nodes-base/nodes/Oracle/Sql/actions/common.descriptions.ts b/packages/nodes-base/nodes/Oracle/Sql/actions/common.descriptions.ts index c8985548af8..043ef80431d 100644 --- a/packages/nodes-base/nodes/Oracle/Sql/actions/common.descriptions.ts +++ b/packages/nodes-base/nodes/Oracle/Sql/actions/common.descriptions.ts @@ -263,6 +263,22 @@ export const optionsCollection: INodeProperties[] = [ description: 'This property is a query tuning option to set the number of additional rows the underlying Oracle driver fetches during the internal initial statement execution phase of a query', }, + { + displayName: 'String OUT Bind Max Size', + name: 'stringOutBindMaxSize', + type: 'number', + default: 4000, + typeOptions: { + minValue: 1, + }, + displayOptions: { + show: { + '/operation': ['execute'], + }, + }, + description: + 'Maximum byte size for string OUT and IN-OUT bind parameters in execute statements', + }, { // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options displayName: 'Output Columns', diff --git a/packages/nodes-base/nodes/Oracle/Sql/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Oracle/Sql/actions/database/executeQuery.operation.ts index 45be5c58a4f..18c087ea372 100644 --- a/packages/nodes-base/nodes/Oracle/Sql/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Oracle/Sql/actions/database/executeQuery.operation.ts @@ -82,7 +82,11 @@ export async function execute( ((this.getNodeParameter('options.params', index, {}) as IDataObject) .values as ExecuteOpBindParam[]) || []; if (parameterIDataObjectList.length) { - const { updatedQuery, bindParameters } = getBindParameters(query, parameterIDataObjectList); + const { updatedQuery, bindParameters } = getBindParameters( + query, + parameterIDataObjectList, + nodeOptions, + ); query = updatedQuery; values = bindParameters; } diff --git a/packages/nodes-base/nodes/Oracle/Sql/helpers/interfaces.ts b/packages/nodes-base/nodes/Oracle/Sql/helpers/interfaces.ts index b6f455ac7fa..8bc5c14f38d 100644 --- a/packages/nodes-base/nodes/Oracle/Sql/helpers/interfaces.ts +++ b/packages/nodes-base/nodes/Oracle/Sql/helpers/interfaces.ts @@ -58,6 +58,7 @@ export type OracleDBNodeOptions = { keepInStmtCache?: boolean; maxRows?: number; prefetchRows?: number; + stringOutBindMaxSize?: number; // n8n options largeNumbersOutputAsString?: boolean; // bigInt diff --git a/packages/nodes-base/nodes/Oracle/Sql/helpers/utils.ts b/packages/nodes-base/nodes/Oracle/Sql/helpers/utils.ts index 1226c2e772a..6419bc534af 100644 --- a/packages/nodes-base/nodes/Oracle/Sql/helpers/utils.ts +++ b/packages/nodes-base/nodes/Oracle/Sql/helpers/utils.ts @@ -27,6 +27,16 @@ import type { TableColumnRow, } from './interfaces'; +type DefaultStringBindParam = Omit< + Extract, + 'datatype' | 'valueString' +> & { + datatype?: undefined; + valueString?: string; +}; + +type RuntimeExecuteOpBindParam = ExecuteOpBindParam | DefaultStringBindParam; + const n8nTypetoDBType: { [key: string]: oracledb.DbType } = { boolean: oracledb.DB_TYPE_BOOLEAN, date: oracledb.DATE, @@ -39,6 +49,8 @@ const n8nTypetoDBType: { [key: string]: oracledb.DbType } = { blob: oracledb.BLOB, }; +export const DEFAULT_STRING_OUT_BIND_MAX_SIZE = 4000; + function isDateType(type: string) { return /^(timestamp(\(\d+\))?( with(?: local)? time zone)?|date)$/i.test(type); } @@ -837,26 +849,59 @@ function isSerializedBuffer(val: unknown): val is { type: 'Buffer'; data: number export function getBindParameters( query: string, parameterList: ExecuteOpBindParam[], + options: Pick = {}, ): { updatedQuery: string; bindParameters: QueryValue } { const bindParameters: ObjectQueryValue = {}; + const stringOutBindMaxSize = options.stringOutBindMaxSize ?? DEFAULT_STRING_OUT_BIND_MAX_SIZE; for (const item of parameterList) { - if (!item.parseInStatement) { + const itemWithOptionalDatatype = item as RuntimeExecuteOpBindParam; + const bindItem = ( + !itemWithOptionalDatatype.datatype + ? { + ...itemWithOptionalDatatype, + datatype: 'string', + valueString: itemWithOptionalDatatype.valueString ?? '', + } + : itemWithOptionalDatatype + ) as ExecuteOpBindParam; + + if (!bindItem.parseInStatement) { let bindVal = null; - const type = item.datatype; + const type = bindItem.datatype; + const dir = + bindItem.bindDirection === 'in' + ? oracledb.BIND_IN + : bindItem.bindDirection === 'out' + ? oracledb.BIND_OUT + : oracledb.BIND_INOUT; + + if (bindItem.bindDirection === 'out') { + const bindParameter: oracledb.BindParameter = { + type: n8nTypetoDBType[type], + dir, + }; + + if (type === 'string') { + bindParameter.maxSize = stringOutBindMaxSize; + } + + bindParameters[bindItem.name] = bindParameter; + continue; + } switch (type) { case 'number': - bindVal = item.valueNumber; + bindVal = bindItem.valueNumber; break; case 'string': - bindVal = item.valueString; + bindVal = bindItem.valueString; break; case 'boolean': - bindVal = item.valueBoolean; + bindVal = bindItem.valueBoolean; break; case 'blob': - bindVal = item.valueBlob; + bindVal = bindItem.valueBlob; // Allow null or undefined to represent SQL NULL BLOB values if (bindVal === null) { @@ -876,7 +921,7 @@ export function getBindParameters( 'BLOB data must be a valid Buffer or \'{ type: "Buffer", data: [...] }\'', ); case 'date': { - const val = item.valueDate; + const val = bindItem.valueDate; if (typeof val === 'string') { bindVal = new Date(val); // string → Date } else if (val instanceof Date) { @@ -890,7 +935,7 @@ export function getBindParameters( break; } case 'sparse': { - const val = item.valueSparse; + const val = bindItem.valueSparse; let indices = val.indices; let values = val.values; const dims = val.dimensions; @@ -922,7 +967,7 @@ export function getBindParameters( } case 'vector': { - const val = item.valueVector; + const val = bindItem.valueVector; bindVal = val; if (typeof val === 'string') { @@ -938,7 +983,7 @@ export function getBindParameters( break; case 'json': { - const val = item.valueJson; + const val = bindItem.valueJson; bindVal = val; if (typeof val === 'string') { @@ -954,19 +999,19 @@ export function getBindParameters( throw new UserError(`Unsupported Bind type: ${type}`); } - const dir = - item.bindDirection === 'in' - ? oracledb.BIND_IN - : item.bindDirection === 'out' - ? oracledb.BIND_OUT - : oracledb.BIND_INOUT; - bindParameters[item.name] = { - type: n8nTypetoDBType[item.datatype], + const bindParameter: oracledb.BindParameter = { + type: n8nTypetoDBType[type], val: bindVal, dir, }; + + if (bindItem.bindDirection === 'inout' && type === 'string') { + bindParameter.maxSize = stringOutBindMaxSize; + } + + bindParameters[bindItem.name] = bindParameter; } else { - query = generateBindVariablesList(item, bindParameters, query); + query = generateBindVariablesList(bindItem, bindParameters, query); } } return { updatedQuery: query, bindParameters }; diff --git a/packages/nodes-base/nodes/Oracle/Sql/test/operations.test.ts b/packages/nodes-base/nodes/Oracle/Sql/test/operations.test.ts index 9f24e305476..54c083915c0 100644 --- a/packages/nodes-base/nodes/Oracle/Sql/test/operations.test.ts +++ b/packages/nodes-base/nodes/Oracle/Sql/test/operations.test.ts @@ -1060,7 +1060,7 @@ VALUES ( OUTENAME: { type: oracleDBTypes.STRING, dir: 3003, - val: undefined, + maxSize: 4000, }, }; diff --git a/packages/nodes-base/nodes/Oracle/Sql/test/utils.test.ts b/packages/nodes-base/nodes/Oracle/Sql/test/utils.test.ts index 8752a935867..a100ffdcdff 100644 --- a/packages/nodes-base/nodes/Oracle/Sql/test/utils.test.ts +++ b/packages/nodes-base/nodes/Oracle/Sql/test/utils.test.ts @@ -237,9 +237,222 @@ describe('Test getBindParameters ', () => { expect(updatedQuery).toEqual(query); expect(bindParameters).toEqual(expectedBindParams); }); + + it('should configure out bind parameters without input values', () => { + const query = 'BEGIN demo(:out_string, :inout_string, :out_number); END;'; + const paramList = [ + { + name: 'out_string', + bindDirection: 'out', + datatype: 'string', + valueString: '', + parseInStatement: false, + }, + { + name: 'inout_string', + bindDirection: 'inout', + datatype: 'string', + valueString: 'input', + parseInStatement: false, + }, + { + name: 'out_number', + bindDirection: 'out', + datatype: 'number', + valueNumber: 0, + parseInStatement: false, + }, + ] as ExecuteOpBindParam[]; + + const { updatedQuery, bindParameters } = getBindParameters(query, paramList); + + expect(updatedQuery).toEqual(query); + expect(bindParameters).toEqual({ + out_string: { + type: oracleDBTypes.STRING, + dir: oracleDBTypes.BIND_OUT, + maxSize: 4000, + }, + inout_string: { + type: oracleDBTypes.STRING, + val: 'input', + dir: oracleDBTypes.BIND_INOUT, + maxSize: 4000, + }, + out_number: { + type: oracleDBTypes.NUMBER, + dir: oracleDBTypes.BIND_OUT, + }, + }); + }); + + it('should use a custom max size for string out bind parameters', () => { + const query = 'BEGIN demo(:out_string, :inout_string); END;'; + const paramList = [ + { + name: 'out_string', + bindDirection: 'out', + datatype: 'string', + valueString: '', + parseInStatement: false, + }, + { + name: 'inout_string', + bindDirection: 'inout', + datatype: 'string', + valueString: 'input', + parseInStatement: false, + }, + ] as ExecuteOpBindParam[]; + + const { updatedQuery, bindParameters } = getBindParameters(query, paramList, { + stringOutBindMaxSize: 1024, + }); + + expect(updatedQuery).toEqual(query); + expect(bindParameters).toEqual({ + out_string: { + type: oracleDBTypes.STRING, + dir: oracleDBTypes.BIND_OUT, + maxSize: 1024, + }, + inout_string: { + type: oracleDBTypes.STRING, + val: 'input', + dir: oracleDBTypes.BIND_INOUT, + maxSize: 1024, + }, + }); + }); + + it('should use the default string type for out bind parameters when the data type is missing', () => { + const query = 'BEGIN demo(:out_string); END;'; + const paramList = [ + { + name: 'out_string', + bindDirection: 'out', + parseInStatement: false, + }, + ] as unknown as ExecuteOpBindParam[]; + + const { updatedQuery, bindParameters } = getBindParameters(query, paramList); + + expect(updatedQuery).toEqual(query); + expect(bindParameters).toEqual({ + out_string: { + type: oracleDBTypes.STRING, + dir: oracleDBTypes.BIND_OUT, + maxSize: 4000, + }, + }); + }); + + it('should use the default string type for out bind parameters when the data type is empty', () => { + const query = 'BEGIN demo(:out_string); END;'; + const paramList = [ + { + name: 'out_string', + bindDirection: 'out', + datatype: '', + parseInStatement: false, + }, + ] as unknown as ExecuteOpBindParam[]; + + const { updatedQuery, bindParameters } = getBindParameters(query, paramList); + + expect(updatedQuery).toEqual(query); + expect(bindParameters).toEqual({ + out_string: { + type: oracleDBTypes.STRING, + dir: oracleDBTypes.BIND_OUT, + maxSize: 4000, + }, + }); + }); + + it('should skip value parsing for out bind parameters', () => { + const query = 'BEGIN demo(:out_blob); END;'; + const paramList = [ + { + name: 'out_blob', + bindDirection: 'out', + datatype: 'blob', + parseInStatement: false, + }, + ] as unknown as ExecuteOpBindParam[]; + + const { updatedQuery, bindParameters } = getBindParameters(query, paramList); + + expect(updatedQuery).toEqual(query); + expect(bindParameters).toEqual({ + out_blob: { + type: oracleDBTypes.BLOB, + dir: oracleDBTypes.BIND_OUT, + }, + }); + }); }); describe('Test configureQueryRunner', () => { + it('should return object out bind values from execute operations', async () => { + const execute = jest.fn().mockResolvedValue({ outBinds: { ret: 'registered' } }); + const close = jest.fn().mockResolvedValue(undefined); + const connection = { execute, close }; + const getConnection = jest.fn().mockResolvedValue(connection); + const pool = { getConnection } as unknown as oracleDBTypes.Pool; + const constructExecutionMetaData = jest + .fn() + .mockImplementation((data: INodeExecutionData[]) => data); + const context = { + helpers: { + constructExecutionMetaData, + }, + } as unknown as IExecuteFunctions; + const node = {} as unknown as INode; + const queryRunner = configureQueryRunner.call(context, node, false, pool); + + const result = await queryRunner( + [ + { + query: 'BEGIN demo(:ret); END;', + values: { + ret: { + type: oracleDBTypes.STRING, + dir: oracleDBTypes.BIND_OUT, + maxSize: 4000, + }, + }, + }, + ], + [], + { + operation: 'execute', + stmtBatching: 'independently', + }, + ); + + expect(result).toEqual([{ json: { ret: 'registered' } }]); + expect(result).not.toEqual([{ json: { success: true } }]); + expect(constructExecutionMetaData).toHaveBeenCalledWith([{ json: { ret: 'registered' } }], { + itemData: { item: 0 }, + }); + expect(getConnection).toHaveBeenCalledTimes(1); + expect(execute).toHaveBeenCalledWith( + 'BEGIN demo(:ret); END;', + { + ret: { + type: oracleDBTypes.STRING, + dir: oracleDBTypes.BIND_OUT, + maxSize: 4000, + }, + }, + expect.objectContaining({ + outFormat: oracleDBTypes.OUT_FORMAT_OBJECT, + }), + ); + expect(close).toHaveBeenCalledTimes(1); + }); + it('should append out-bind execution data one item at a time without spread push', async () => { const pushSpy = jest.spyOn(Array.prototype, 'push'); const outBinds = [