mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-21 20:20:03 +02:00
fix(Oracle Database Node): Support OUT bind parameters (#30083)
Co-authored-by: Garrit Franke <32395585+garritfra@users.noreply.github.com> Co-authored-by: Jon <jonathan.bennetts@gmail.com>
This commit is contained in:
parent
24c264ca28
commit
d3406dd75f
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export type OracleDBNodeOptions = {
|
|||
keepInStmtCache?: boolean;
|
||||
maxRows?: number;
|
||||
prefetchRows?: number;
|
||||
stringOutBindMaxSize?: number;
|
||||
|
||||
// n8n options
|
||||
largeNumbersOutputAsString?: boolean; // bigInt
|
||||
|
|
|
|||
|
|
@ -27,6 +27,16 @@ import type {
|
|||
TableColumnRow,
|
||||
} from './interfaces';
|
||||
|
||||
type DefaultStringBindParam = Omit<
|
||||
Extract<ExecuteOpBindParam, { datatype: 'string' }>,
|
||||
'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<OracleDBNodeOptions, 'stringOutBindMaxSize'> = {},
|
||||
): { 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 };
|
||||
|
|
|
|||
|
|
@ -1060,7 +1060,7 @@ VALUES (
|
|||
OUTENAME: {
|
||||
type: oracleDBTypes.STRING,
|
||||
dir: 3003,
|
||||
val: undefined,
|
||||
maxSize: 4000,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user