import get from 'lodash/get'; import { type IDataObject, type IExecuteFunctions, type IGetNodeParameterOptions, type INode, type INodeExecutionData, type INodeParameters, } from 'n8n-workflow'; import * as oracleDBTypes from 'oracledb'; import * as deleteTable from '../actions/database/deleteTable.operation'; import * as executeSQL from '../actions/database/executeQuery.operation'; import * as insert from '../actions/database/insert.operation'; import * as select from '../actions/database/select.operation'; import * as update from '../actions/database/update.operation'; import * as upsert from '../actions/database/upsert.operation'; import type { OracleDBNodeCredentials, QueryWithValues } from '../helpers/interfaces'; import { configureQueryRunner } from '../helpers/utils'; import { configureOracleDB } from '../transport'; const mockConnection = { execute: jest.fn((_query = '') => { const result = {} as { rows: any[] }; result.rows = [ { COLUMN_NAME: 'AGE', DATA_TYPE: 'NUMBER', DATA_LENGTH: 22, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, ROW_EXISTS: 1, // for letting update tests to go through in simulation mode }, { COLUMN_NAME: 'EMBEDDING', DATA_TYPE: 'VECTOR', DATA_LENGTH: 8200, CHAR_LENGTH: 3, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'ID', DATA_TYPE: 'NUMBER', DATA_LENGTH: 22, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'N', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: 'C', }, { COLUMN_NAME: 'IS_ACTIVE', DATA_TYPE: 'BOOLEAN', DATA_LENGTH: 1, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'JOIN_DATE', DATA_TYPE: 'DATE', DATA_LENGTH: 7, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'META_DATA', DATA_TYPE: 'JSON', DATA_LENGTH: 8200, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'NAME', DATA_TYPE: 'VARCHAR2', DATA_LENGTH: 100, CHAR_LENGTH: 100, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'PICTURE', DATA_TYPE: 'BLOB', DATA_LENGTH: 4000, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'PROFILE', DATA_TYPE: 'RAW', DATA_LENGTH: 2000, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, { COLUMN_NAME: 'SALARY', DATA_TYPE: 'FLOAT', DATA_LENGTH: 22, CHAR_LENGTH: 0, DEFAULT_LENGTH: null, NULLABLE: 'Y', IDENTITY_COLUMN: 'NO', HAS_DEFAULT: 'NO', CONSTRAINT_TYPES: null, }, ]; return result; }), close: jest.fn(), beginTransaction: jest.fn(), commit: jest.fn(), rollback: jest.fn(), }; Object.defineProperty(mockConnection, 'oracleServerVersion', { get: () => 2305000000, // mimic server is 23ai }); const createFakePool = (connection: IDataObject) => { return { getConnection() { return connection; }, } as unknown as oracleDBTypes.Pool; }; const node: INode = { id: '1', typeVersion: 1, name: 'Oracle Database node', type: 'n8n-nodes-starter-oracledb', position: [60, 760], parameters: { operation: 'execute', }, }; // Note: The password is read from environment just for testing purpose. // In practice, it often uses some secure storage mechanism, // such as Oracle Cloud Infrastructure (OCI) Vault or OCI Key Management Service (KMS),... const CONFIG = { user: process.env.ORACLE_USER ?? 'VECTOR', password: process.env.ORACLE_PASSWORD ?? 'vector', connectionString: process.env.ORACLE_CONNECTSTRING ?? 'localhost:1521/freepdb1', }; let continueOnFail = true; let queries: QueryWithValues[] = []; let queriesIndependent: QueryWithValues[] = []; const integratedTests = process.env.ORACLE_INTEG_TESTS ?? false; type RunQueriesFn = ( queries: QueryWithValues[], inputItems: INodeExecutionData[], nodeOptions: IDataObject, ) => Promise; const createMockExecuteFunction = (nodeParameters: IDataObject) => { const fakeExecuteFunction = { getNodeParameter( parameterName: string, _itemIndex: number, fallbackValue?: IDataObject, options?: IGetNodeParameterOptions, ) { if (parameterName === 'columns.value') { const columns = nodeParameters.columns as IDataObject; const value = columns?.value; if (Array.isArray(value)) { return value[_itemIndex]; } // fallback if not an array return value; } const parameter = options?.extractValue ? `${parameterName}.value` : parameterName; return get(nodeParameters, parameter, fallbackValue); }, getNode() { node.parameters = { ...node.parameters, ...(nodeParameters as INodeParameters) }; return node; }, getCredentials: () => { return CONFIG; }, continueOnFail: () => { return continueOnFail; }, helpers: { constructExecutionMetaData: jest.fn((data) => data), }, } as unknown as IExecuteFunctions; return fakeExecuteFunction; }; export function getRunQueriesFn( mockThis: any, pool: oracleDBTypes.Pool, ): jest.Mock, Parameters> { if (integratedTests) { // Create the real query runner using the node context const realRunQueries: RunQueriesFn = configureQueryRunner.call( mockThis, mockThis.getNode(), mockThis.continueOnFail(), pool, ); // Wrap the real one with a spy to track calls return jest.fn(async (...args: Parameters) => { return await realRunQueries(...args); }); } // Default: return mock function with correct signature return jest.fn, Parameters>(); } describe('Test All operations', () => { let pool: oracleDBTypes.Pool; const nodeParametersDef: IDataObject = { operation: 'execute', }; const integratedTestsTimeOut = 200000; // connecting to DB and test it. const mockThisDef = createMockExecuteFunction(nodeParametersDef); const table = 'N8N_TEST_DEMO_TYPES'; const deptTable = 'N8N_TEST_DEPT'; const maxEmpName = 100; const createTbl = `CREATE TABLE if not exists ${table} ( id NUMBER NOT NULL, name VARCHAR2(${maxEmpName}), age NUMBER(38), salary FLOAT(126), join_date DATE, is_active BOOLEAN, meta_data JSON, embedding VECTOR(3, FLOAT32, DENSE), picture BLOB, profile RAW(2000) )`; const dropTbl = `DROP TABLE if exists ${table}`; const truncateTbl = `TRUNCATE TABLE ${table}`; const createDeptTbl = `CREATE TABLE if not exists ${deptTable} ( deptno NUMBER, empname VARCHAR2(100), location VARCHAR2(100))`; const truncateDeptTbl = `TRUNCATE TABLE ${deptTable}`; const insertSqls = [ `INSERT INTO ${deptTable} (deptno, empname, location) VALUES (10, 'Alice', 'New York')`, `INSERT INTO ${deptTable} (deptno, empname, location) VALUES (10, 'Bob', 'New York')`, `INSERT INTO ${deptTable} (deptno, empname, location) VALUES (20, 'Charlie', 'Boston')`, `INSERT INTO ${table} ( id, name, age, salary, join_date, is_active, meta_data, embedding, picture, profile ) VALUES ( 1, 'Alice', 30, 75000.0, DATE '2020-01-15', TRUE, JSON_OBJECT('department' VALUE 'HR', 'location' VALUE 'New York' RETURNING JSON), VECTOR('[0.12, 0.34, 0.56]'), UTL_RAW.CAST_TO_RAW('binary_data_here for BLOB1'), UTL_RAW.CAST_TO_RAW('binary_data_here for RAW1') )`, `INSERT INTO ${table} ( id, name, age, salary, join_date, is_active, meta_data, embedding, picture, profile ) VALUES ( 2, 'Bob', 45, 92000.5, DATE '2018-07-22', FALSE, JSON_OBJECT('department' VALUE 'Engineering', 'level' VALUE 3 RETURNING JSON), VECTOR('[0.78, 0.65, 0.43]'), UTL_RAW.CAST_TO_RAW('binary_data_here for BLOB2'), UTL_RAW.CAST_TO_RAW('binary_data_here for RAW2') )`, `INSERT INTO ${table} ( id, name, age, salary, join_date, is_active, meta_data, embedding, picture, profile ) VALUES ( 3, 'Charlie', 28, 64000, DATE '2021-11-10', TRUE, JSON_OBJECT('department' VALUE 'Finance', 'location' VALUE 'Mumbai' RETURNING JSON), VECTOR('[0.5, 0.5, 0.5]'), UTL_RAW.CAST_TO_RAW('binary_data_here for BLOB3'), UTL_RAW.CAST_TO_RAW('binary_data_here for RAW3') )`, ]; const dropDeptTbl = `DROP TABLE if exists ${deptTable}`; if (integratedTests) { jest.setTimeout(integratedTestsTimeOut); } async function populateRows(conn: oracleDBTypes.Connection) { if (conn) { for (const sql of insertSqls) { await conn.execute(sql, [], { autoCommit: true }); } } } async function setup() { if (pool) { const conn = await pool.getConnection(); await conn.execute(createTbl); await conn.execute(createDeptTbl); await populateRows(conn); await conn.close(); } } /** * Normalizes bind parameter maps by grouping and simplifying * parameter names that share a common base but differ by unique suffixes (came from uuid). * * const input = { * Namese10bdb52_9f57_42c3_bfd4_ed880b94e186: { type: oracledb.STRING, val: "Alice" }, * Names323b551b_0d6a_4657_8928_435f961554f1: { type: oracledb.STRING, val: "Bob" }, * Dno: { type: oracledb.NUMBER, val: 10, dir: 3002 }, * }; * * const output = normalizeParams(input); * // output: * // { * // Names: ["Alice", "Bob"], * // Dno: 10 * // } */ function normalizeParams(params: Record | undefined) { if (!params) return params; const normalized: Record = {}; const groupMap: Record = {}; for (const [key, val] of Object.entries(params)) { const baseKey = key.replace(/[_\dA-Fa-f-]+$/, ''); // remove unique suffixes (UUID fragments,..) if (!groupMap[baseKey]) groupMap[baseKey] = []; groupMap[baseKey].push(val && typeof val === 'object' && 'val' in val ? val.val : val); } for (const [baseKey, values] of Object.entries(groupMap)) { // If there’s only one scalar value, store it directly normalized[baseKey] = values.length === 1 ? values[0] : [...values].sort(); } return normalized; } const getInputItems = (items: any[]) => items.map((v) => ({ json: v })); const patchMockParams = ( items: INodeExecutionData[], nodePars: IDataObject, mappingMode: 'defineBelow' | 'autoMapInputData' = 'defineBelow', ) => { (nodePars.columns as any).value = structuredClone(items); (nodePars.columns as any).mappingMode = mappingMode; return createMockExecuteFunction(nodePars); }; function verifyOutPutColumnsOptions( runQueries: jest.Mock, Parameters>, nodeOptions: IDataObject, inputItems: INodeExecutionData[], result: INodeExecutionData[], ) { if ( integratedTests && Array.isArray(nodeOptions.outputColumns) && ((nodeOptions.outputColumns as string[]).includes('EMBEDDING') || (nodeOptions.outputColumns as string[]).includes('*')) ) { if (nodeOptions.stmtBatching === 'single') { expect(runQueries).toHaveBeenCalledWith(queries, inputItems, nodeOptions); } else { expect(runQueries).toHaveBeenCalledWith(queriesIndependent, inputItems, nodeOptions); } let actualEmbedding, actualID; if ((nodeOptions.outputColumns as string[]).includes('*')) { expect( result.map(({ json }) => { const { PICTURE, ...rest } = json; // skip comparing PICTURE column which is LOB return { ...rest, EMBEDDING: new Float32Array(rest.EMBEDDING as number[]), PROFILE: Buffer.from(rest.PROFILE as Buffer), }; }), ).toEqual( inputItems.map(({ json }) => { const { PICTURE, ...rest } = json; return { ...rest, EMBEDDING: new Float32Array(rest.EMBEDDING as number[]), PROFILE: Buffer.from(rest.PROFILE as Buffer), }; }), ); } else { for (let i = 0, j = 0; i < result.length; ) { const expectedEmbedding = inputItems[j].json.EMBEDDING; if (nodeOptions.stmtBatching === 'single') { actualEmbedding = result[i]?.json?.EMBEDDING; actualID = result[i]?.json?.ID; } else { actualEmbedding = result[i]?.json.EMBEDDING; actualID = result[i]?.json.ID; } expect(actualEmbedding).toEqual(new Float32Array(expectedEmbedding as number[])); const expectedID = inputItems[j].json.ID; expect(actualID).toEqual(expectedID as number); i = i + 2; j = j + 1; } } } } const runOperation = async ({ operation = insert, items, nodePars, nodeOptions, continueOnFailValue, mappingMode = 'defineBelow', failedIndex = 1, errorPattern, }: { items: any[]; continueOnFailValue: boolean; mappingMode?: 'defineBelow' | 'autoMapInputData'; nodeOptions: IDataObject; errorPattern?: RegExp; failedIndex?: number; operation?: any; nodePars: IDataObject; }) => { const inputItems = getInputItems(items); const mock = patchMockParams(items, nodePars, mappingMode); continueOnFail = continueOnFailValue; const runQueries = getRunQueriesFn(mock, pool); if (errorPattern && !continueOnFailValue && integratedTests) { await expect( operation.execute.call(mock, runQueries, inputItems, nodeOptions, pool), ).rejects.toThrow(errorPattern); return; } const result = await operation.execute.call(mock, runQueries, inputItems, nodeOptions, pool); expect(runQueries).toHaveBeenCalled(); // check the arguments passed to runQueries are as expected. const [_, actualItems, actualOptions] = runQueries.mock.calls[0]; actualItems.forEach((item, index) => { expect(item.json).toEqual(items[index]); }); expect(actualOptions).toBe(nodeOptions); if (!errorPattern && Array.isArray(nodeOptions.outputColumns)) { if (nodeOptions.stmtBatching === 'single') { expect(runQueries).toHaveBeenCalledWith(queries, inputItems, nodeOptions); } else { expect(runQueries).toHaveBeenCalledWith(queriesIndependent, inputItems, nodeOptions); } // Validate outputColumns match for successful batch insert. verifyOutPutColumnsOptions(runQueries, nodeOptions, inputItems, result); } // Validate results based on stmtBatching if (integratedTests) { if (errorPattern && continueOnFailValue) { if (nodeOptions.stmtBatching === 'single') { // For single , we just show batch errors. expect(result[0].json.message).toMatch(errorPattern); expect(result[0].pairedItem.item).toBe(failedIndex); } else { // For independent, we return successful outbinds and also an // error message for failed items. let index = 0; if (nodeOptions.outputColumns) { // For outputcolumns option, we pass second item index with wrong values. index = 1; } expect(result[index].json.message).toMatch(errorPattern); expect(result[index].pairedItem.item).toBe(failedIndex); } } } }; beforeEach(async function () { if (integratedTests) { if (pool) { const conn = await pool.getConnection(); await conn.execute(truncateTbl); await conn.execute(truncateDeptTbl); await populateRows(conn); await conn.close(); } } }); beforeAll(async function () { if (integratedTests) { const credentials = await mockThisDef.getCredentials('oracleDBApi'); pool = await configureOracleDB.call(mockThisDef, credentials, {}); await setup(); } else { pool = createFakePool(mockConnection); } }); afterAll(async function () { if (integratedTests) { const conn = await pool.getConnection(); await conn.execute(dropTbl); await conn.execute(dropDeptTbl); await conn.close(); await pool?.close(); } }); it('should have all operations', () => { expect(deleteTable.execute).toBeDefined(); expect(deleteTable.description).toBeDefined(); expect(executeSQL.execute).toBeDefined(); expect(executeSQL.description).toBeDefined(); expect(insert.execute).toBeDefined(); expect(insert.description).toBeDefined(); expect(select.execute).toBeDefined(); expect(select.description).toBeDefined(); expect(update.execute).toBeDefined(); expect(update.description).toBeDefined(); expect(upsert.execute).toBeDefined(); expect(upsert.description).toBeDefined(); }); describe('Test delete operation', () => { afterEach(() => { jest.clearAllMocks(); }); const docid = 1; const testCases = [ { condition: 'LIKE', value: docid, expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" LIKE :0`, bindVal: { type: oracleDBTypes.NUMBER, val: docid }, }, { condition: '>', value: docid, expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" > :0`, bindVal: { type: oracleDBTypes.NUMBER, val: docid }, }, { condition: 'equal', value: docid, expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" = :0`, bindVal: { type: oracleDBTypes.NUMBER, val: docid }, }, { condition: 'IS NULL', expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" IS NULL`, }, { condition: 'IS NOT NULL', expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" IS NOT NULL`, }, { condition: 'IS NOT NULL', expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" IS NOT NULL`, options: { stmtBatching: 'transaction', }, }, { condition: 'equal', value: docid, expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" = :0`, executeManyValues: [[1]], options: { bindDefs: [{ type: oracleDBTypes.NUMBER }], stmtBatching: 'single', }, }, { condition: 'IS NOT NULL', expectedQuery: `DELETE FROM "${CONFIG.user}"."${table}" WHERE "ID" IS NOT NULL`, executeManyValues: [[undefined]], options: { bindDefs: [], stmtBatching: 'single', }, }, ]; it.each(testCases)( 'should handle condition: %s', async ({ condition, value, expectedQuery, bindVal, executeManyValues, options = { stmtBatching: 'independently', }, }) => { const nodeParameters: IDataObject = { operation: 'deleteTable', schema: { __rl: true, mode: 'list', value: CONFIG.user, }, table: { __rl: true, value: table, mode: 'list', cachedResultName: table, }, options, deleteCommand: 'delete', where: { values: [ { column: 'ID', condition, value, }, ], }, }; const nodeOptions = nodeParameters.options as IDataObject; const mockThis = createMockExecuteFunction(nodeParameters); const emptyInputItems = [{ json: {}, pairedItem: { item: 0, input: undefined } }]; const runQueries = getRunQueriesFn(mockThis, pool); await deleteTable.execute.call(mockThis, runQueries, emptyInputItems, nodeOptions, pool); const queries = []; if (executeManyValues) { queries.push({ query: expectedQuery, executeManyValues, }); } else { queries.push({ query: expectedQuery, values: [bindVal], }); } expect(runQueries).toHaveBeenCalledWith(queries, emptyInputItems, nodeOptions); }, ); }); describe('Test execute operation', () => { afterEach(() => { jest.clearAllMocks(); }); it('should call runQueries with binds passed using bindInfo and single item', async () => { const expectedQuery = 'SELECT :en as name, dname from dept where deptno = :dno'; const items = [ { json: { DEPTNO: 10, EMPNAME: 'ALICE', }, pairedItem: { item: 0, input: undefined, }, }, ]; const nodeParameters: IDataObject = { operation: 'execute', query: expectedQuery, isBindInfo: true, resource: 'database', options: { params: { values: [ { name: 'Dno', // JSON Expression like value: "={{ $json.DEPTNO }}" needs // to mock getParameterValue, so giving value directly. valueNumber: 10, datatype: 'number', parseInStatement: false, }, { name: 'En', valueString: 'ALICE', datatype: 'string', parseInStatement: false, }, ], }, }, }; const mockThis = createMockExecuteFunction(nodeParameters); const runQueries = getRunQueriesFn(mockThis, pool); const nodeOptions = nodeParameters.options as IDataObject; await executeSQL.execute.call(mockThis, runQueries, items, nodeOptions, pool); // Assert that runQueries was called with expected query and bind values expect(runQueries).toHaveBeenCalledTimes(1); const callArgs = runQueries.mock.calls[0] as [QueryWithValues[], unknown, unknown]; const [expectedArgs] = callArgs; const val = { Dno: { type: oracleDBTypes.NUMBER, val: 10, dir: 3002, }, En: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'ALICE', dir: 3002, }, }; expect(expectedArgs).toHaveLength(1); expect(expectedArgs[0].query).toBe(expectedQuery); expect(expectedArgs[0].values).toEqual(val); }); it.each([ { title: 'should call runQueries with binds passed using bindInfo with parseIn as true', queryTemplate: (deptTable: string) => `SELECT * FROM ${deptTable} WHERE deptno = :Dno AND empname IN :Names`, bindValues: [ { name: 'Dno', valueNumber: 10, datatype: 'number', parseInStatement: false }, { name: 'Names', valueString: 'Alice,Bob', datatype: 'string', parseInStatement: true }, ], expectedVal: { Dno: { type: oracleDBTypes.NUMBER, val: 10, dir: 3002 }, Names: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Alice', dir: 3002 }, Names2: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Bob', dir: 3002 }, }, expectedRegex: /^SELECT \* FROM N8N_TEST_DEPT WHERE deptno = :Dno AND empname IN \(:Names[\w_]+(,:Names[\w_]+)*\)$/, }, { title: 'should call runQueries with binds passed using bindInfo with parseIn as true with _ in bindname', queryTemplate: (deptTable: string) => `SELECT * FROM ${deptTable} WHERE deptno = :Dno AND empname IN :Names_`, bindValues: [ { name: 'Dno', valueNumber: 10, datatype: 'number', parseInStatement: false }, { name: 'Names_', valueString: 'Alice,Bob', datatype: 'string', parseInStatement: true }, ], expectedVal: { Dno: { type: oracleDBTypes.NUMBER, val: 10, dir: 3002 }, Names_: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Alice', dir: 3002 }, Names_2: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Bob', dir: 3002 }, }, expectedRegex: /^SELECT \* FROM N8N_TEST_DEPT WHERE deptno = :Dno AND empname IN \(:Names_[\w_]+(,:Names_[\w_]+)*\)$/, }, { title: 'should call runQueries with binds passed using bindInfo with parseIn as true with $ in bindname', queryTemplate: (deptTable: string) => `SELECT * FROM ${deptTable} WHERE deptno = :Dno AND empname IN :Names_ex2$`, bindValues: [ { name: 'Dno', valueNumber: 10, datatype: 'number', parseInStatement: false }, { name: 'Names_ex2$', valueString: 'Alice,Bob', datatype: 'string', parseInStatement: true, }, ], expectedVal: { Dno: { type: oracleDBTypes.NUMBER, val: 10, dir: 3002 }, Names_ex2$1: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Alice', dir: 3002 }, Names_ex2$2: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Bob', dir: 3002 }, }, expectedRegex: /^SELECT \* FROM N8N_TEST_DEPT WHERE deptno = :Dno AND empname IN \(:Names_ex2\$[\w_]+(,:Names_ex2\$[\w_]+)*\)$/, }, { title: 'should call runQueries with binds passed using partial matching bindInfo with parseIn as true', queryTemplate: (deptTable: string) => `SELECT * FROM ${deptTable} WHERE deptno = :Names1 AND empname IN :Names`, bindValues: [ { name: 'Names1', valueNumber: 10, datatype: 'number', parseInStatement: false }, { name: 'Names', valueString: 'Alice,Bob', datatype: 'string', parseInStatement: true }, ], expectedVal: { Names1: { type: oracleDBTypes.NUMBER, val: 10, dir: 3002 }, Names: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Alice', dir: 3002 }, Names2: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Bob', dir: 3002 }, }, expectedRegex: /^SELECT \* FROM N8N_TEST_DEPT WHERE deptno = :Names1 AND empname IN \(:Names[\w_]+(,:Names[\w_]+)*\)$/, }, { title: 'should correctly expand IN clause for single scalar bind value (SELECT :param1 ...)', queryTemplate: () => 'SELECT :param1 FROM DUAL WHERE DUMMY IN :param1', bindValues: [ { name: 'param1', valueString: 'X', datatype: 'string', parseInStatement: true }, ], expectedVal: { param1: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'X', dir: 3002 }, }, expectedRegex: /^SELECT\s*\(?:param1[\w_]*\)?\s*FROM\s*DUAL\s*WHERE\s*DUMMY\s*IN\s*\(:param1[\w_]*\)$/, }, { title: 'should correctly handle SELECT * FROM dual where DUMMY in :param1', queryTemplate: () => 'SELECT * FROM DUAL WHERE DUMMY IN :param1', bindValues: [ { name: 'param1', valueString: 'X,Y', datatype: 'string', parseInStatement: true }, ], expectedVal: { param1: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'X', dir: 3002 }, param12: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'Y', dir: 3002 }, }, expectedRegex: /^SELECT \* FROM DUAL WHERE DUMMY IN \(:param1[\w_]+(,:param1[\w_]+)*\)$/, }, ])('$title', async ({ queryTemplate, bindValues, expectedVal, expectedRegex }) => { const expectedQuery = queryTemplate(deptTable); const items = [ { json: { DEPTNO: 10, EMPNAME: 'Alice, Bob' }, pairedItem: { item: 0, input: undefined }, }, ]; const nodeParameters: IDataObject = { operation: 'execute', query: expectedQuery, isBindInfo: true, resource: 'database', options: { params: { values: bindValues }, }, }; const mockThis = createMockExecuteFunction(nodeParameters); const nodeOptions = nodeParameters.options as IDataObject; const runQueries = getRunQueriesFn(mockThis, pool); const result = await executeSQL.execute.call(mockThis, runQueries, items, nodeOptions, pool); if (integratedTests && expectedQuery.includes('N8N_TEST_DEPT')) { // Only check DB data when using the department table. const normalize = (arr: any) => arr .map((e: { json: any }) => e.json) .sort((a: { EMPNAME: string }, b: { EMPNAME: any }) => a.EMPNAME.localeCompare(b.EMPNAME), ); expect(normalize(result)).toEqual( normalize([ { json: { DEPTNO: 10, EMPNAME: 'Bob', LOCATION: 'New York' } }, { json: { DEPTNO: 10, EMPNAME: 'Alice', LOCATION: 'New York' } }, ]), ); } expect(runQueries).toHaveBeenCalledTimes(1); const callArgs = runQueries.mock.calls[0] as [QueryWithValues[], unknown, unknown]; const [expectedArgs] = callArgs; expect(expectedArgs).toHaveLength(1); expect(expectedArgs[0].query).toMatch(expectedRegex); expect(normalizeParams(expectedArgs[0].values)).toEqual(normalizeParams(expectedVal)); }); it('should call runQueries with out binds in plsql using bindInfo and multiple items', async () => { const expectedQuery = ` BEGIN :out_value1 := :in_value * 2; :out_value2 := :in_value + 10; :out_value3 := :in_value * :in_value; END;`; const makeItem = (i: number) => ({ json: { in_value: 10 }, pairedItem: { item: i, input: undefined }, }); const items = [makeItem(0), makeItem(1)]; const outParams = ['out_value1', 'out_value2', 'out_value3']; const nodeParameters: IDataObject = { operation: 'execute', query: expectedQuery, isBindInfo: true, resource: 'database', options: { params: { values: [ { name: 'in_value', valueNumber: 10, datatype: 'number', parseInStatement: false, }, ...outParams.map((name) => ({ name, datatype: 'number', parseInStatement: false, bindDirection: 'out', })), ], }, }, }; const mockThis = createMockExecuteFunction(nodeParameters); const runQueries = getRunQueriesFn(mockThis, pool); const nodeOptions = nodeParameters.options as IDataObject; const result = await executeSQL.execute.call(mockThis, runQueries, items, nodeOptions, pool); if (integratedTests) { for (const r of result) { expect(r.json).toMatchObject({ out_value1: 20, out_value2: 20, out_value3: 100, }); } } expect(runQueries).toHaveBeenCalledTimes(1); const [calls] = runQueries.mock.calls[0] as [QueryWithValues[], unknown, unknown]; expect(calls).toHaveLength(2); const expectedValues = { in_value: { type: oracleDBTypes.NUMBER, val: 10, dir: 3002 }, out_value1: { type: oracleDBTypes.NUMBER, dir: 3003 }, out_value2: { type: oracleDBTypes.NUMBER, dir: 3003 }, out_value3: { type: oracleDBTypes.NUMBER, dir: 3003 }, }; calls.forEach((arg) => { expect(arg.query).toBe(expectedQuery); expect(arg.values).toEqual(expectedValues); }); }); it('should call runQueries with binds passed in sql using bindInfo and single item', async () => { const expectedQuery = `INSERT INTO ${deptTable} (DEPTNO, EMPNAME) VALUES(:DNO, :ENAME) RETURNING EMPNAME INTO :OUTENAME`; const items = [ { json: { DEPTNO: 100, EMPNAME: 'ALICE', }, pairedItem: { item: 0, input: undefined, }, }, ]; const nodeParameters: IDataObject = { operation: 'execute', query: expectedQuery, isBindInfo: true, resource: 'database', options: { params: { values: [ { name: 'DNO', valueNumber: 100, datatype: 'number', parseInStatement: false, }, { name: 'ENAME', valueString: 'ALICE', datatype: 'string', parseInStatement: false, }, { name: 'OUTENAME', datatype: 'string', bindDirection: 'out', parseInStatement: false, }, ], }, }, }; const mockThis = createMockExecuteFunction(nodeParameters); const runQueries = getRunQueriesFn(mockThis, pool); const nodeOptions = nodeParameters.options as IDataObject; const result = await executeSQL.execute.call(mockThis, runQueries, items, nodeOptions, pool); if (integratedTests) { for (const r of result) { expect(r.json).toMatchObject({ DNO: 100, ENAME: 'ALICE', OUTENAME: 'ALICE', }); } } // Assert that runQueries was called with expected query and bind values expect(runQueries).toHaveBeenCalledTimes(1); const callArgs = runQueries.mock.calls[0] as [QueryWithValues[], unknown, unknown]; const [expectedArgs] = callArgs; const val = { DNO: { type: oracleDBTypes.NUMBER, val: 100, dir: 3002, }, ENAME: { type: oracleDBTypes.DB_TYPE_VARCHAR, val: 'ALICE', dir: 3002, }, OUTENAME: { type: oracleDBTypes.STRING, dir: 3003, val: undefined, }, }; expect(expectedArgs).toHaveLength(1); expect(expectedArgs[0].query).toBe(expectedQuery); expect(expectedArgs[0].values).toEqual(val); }); it('should bind NULL, valid, and mixed values correctly for all supported column types', async () => { const expectedQuery = `INSERT INTO ${table} ( id, name, age, salary, join_date, is_active, meta_data, embedding, picture) VALUES ( :ID, :NAME, :AGE, :SALARY, :JOIN_DATE, :IS_ACTIVE, :META_DATA, :EMBEDDING, :PICTURE)`; const DOJ = '2024-01-15T00:00:00.000Z'; const inputs = [ { label: 'non-null values', expectedId: 1, items: [ { json: { ID: 1, NAME: 'Alice', AGE: 30, SALARY: 75000.5, JOIN_DATE: DOJ, IS_ACTIVE: true, META_DATA: { team: 'AI', level: 5 }, EMBEDDING: [0.1, 0.2, 0.3], PICTURE: Buffer.from('hello world'), }, }, ], params: [ { name: 'ID', valueNumber: 1, datatype: 'number' }, { name: 'NAME', valueString: 'Alice', datatype: 'string' }, { name: 'AGE', valueNumber: 30, datatype: 'number' }, { name: 'SALARY', valueNumber: 75000.5, datatype: 'number' }, { name: 'JOIN_DATE', valueDate: DOJ, datatype: 'date' }, { name: 'IS_ACTIVE', valueBoolean: true, datatype: 'boolean' }, { name: 'META_DATA', valueJson: { team: 'AI', level: 5 }, datatype: 'json' }, { name: 'EMBEDDING', valueVector: [0.1, 0.2, 0.3], datatype: 'vector' }, { name: 'PICTURE', valueBlob: Buffer.from('hello world'), datatype: 'blob' }, ], }, { label: 'null values', expectedId: 2, items: [ { json: { ID: 2, NAME: null, AGE: null, SALARY: null, JOIN_DATE: null, IS_ACTIVE: null, META_DATA: null, EMBEDDING: null, PICTURE: null, }, }, ], params: [ { name: 'ID', valueNumber: 2, datatype: 'number' }, { name: 'NAME', valueString: null, datatype: 'string' }, { name: 'AGE', valueNumber: null, datatype: 'number' }, { name: 'SALARY', valueNumber: null, datatype: 'number' }, { name: 'JOIN_DATE', valueDate: null, datatype: 'date' }, { name: 'IS_ACTIVE', valueBoolean: null, datatype: 'boolean' }, { name: 'META_DATA', valueJson: null, datatype: 'json' }, { name: 'EMBEDDING', valueVector: null, datatype: 'vector' }, { name: 'PICTURE', valueBlob: null, datatype: 'blob' }, ], }, { label: 'mixed values', expectedId: 3, items: [ { json: { ID: 3, NAME: null, // mix of null + valid AGE: 45, SALARY: null, JOIN_DATE: DOJ, IS_ACTIVE: false, META_DATA: null, EMBEDDING: [0.5, 0.6, 3.4], PICTURE: { type: 'Buffer', data: [14, 15, 16] }, }, }, ], params: [ { name: 'ID', valueNumber: 3, datatype: 'number' }, { name: 'NAME', valueString: null, datatype: 'string' }, { name: 'AGE', valueNumber: 45, datatype: 'number' }, { name: 'SALARY', valueNumber: null, datatype: 'number' }, { name: 'JOIN_DATE', valueDate: DOJ, datatype: 'date' }, { name: 'IS_ACTIVE', valueBoolean: false, datatype: 'boolean' }, { name: 'META_DATA', valueJson: null, datatype: 'json' }, { name: 'EMBEDDING', valueVector: [0.5, 0.6, 3.4], datatype: 'vector' }, { name: 'PICTURE', valueBlob: { type: 'Buffer', data: [14, 15, 16] }, datatype: 'blob', }, ], }, ]; for (const input of inputs) { const nodeParameters: IDataObject = { operation: 'execute', query: expectedQuery, isBindInfo: true, resource: 'database', options: { params: { values: input.params, }, }, }; const mockThis = createMockExecuteFunction(nodeParameters); const runQueries = getRunQueriesFn(mockThis, pool); const nodeOptions = nodeParameters.options as IDataObject; const result = await executeSQL.execute.call( mockThis, runQueries, input.items, nodeOptions, pool, ); expect(runQueries).toHaveBeenCalled(); const [expectedArgs] = runQueries.mock.calls.pop() as [QueryWithValues[], unknown, unknown]; expect(expectedArgs).toHaveLength(1); expect(expectedArgs[0].query).toBe(expectedQuery); const binds = expectedArgs[0].values; // Dynamic assertions per input type for (const [key, bind] of Object.entries(binds as Record)) { if (key === 'ID') { expect(bind.val).toBe(input.expectedId); } else { const original = (input.items[0].json as Record)[key]; if (original === null || original === undefined) { expect(bind.val).toBeNull(); } else if (key === 'JOIN_DATE') { expect(bind.val instanceof Date).toBe(true); expect((bind.val as Date).toISOString()).toBe(DOJ); } else if (key === 'PICTURE' && original.type === 'Buffer') { // serialized form expect(bind.val).toEqual(Buffer.from(original)); } else { expect(bind.val).toEqual(original); } } } // Optional integrated check if (integratedTests) { const row = result[0].json; expect(row.ID).toBe(input.expectedId); for (const [key, val] of Object.entries(row)) { if (key === 'ID' || key === 'PICTURE') continue; const original = (input.items[0].json as Record)[key]; if (original === null || original === undefined) { expect(val === null || val === undefined).toBe(true); } else if (key === 'JOIN_DATE') { expect((val as Date).toISOString()).toBe(DOJ); } else if (key === 'META_DATA') { continue; } else if (key === 'EMBEDDING') { expect(Array.from(val as Float64Array)).toEqual(original); } else { expect(val).toEqual(original); } } } } }); }); describe('Test select operation', () => { const items = [{ json: {} }]; afterEach(() => { jest.clearAllMocks(); }); it('returnAll, should call runQueries with', async () => { const nodeParameters: IDataObject = { operation: 'select', schema: { __rl: true, mode: 'list', value: CONFIG.user, }, table: { __rl: true, value: table, mode: 'list', cachedResultName: table, }, returnAll: true, options: {}, }; const mockThis = createMockExecuteFunction(nodeParameters); const runQueries = getRunQueriesFn(mockThis, pool); const nodeOptions = nodeParameters.options as IDataObject; await select.execute.call(mockThis, runQueries, items, nodeOptions, pool); expect(runQueries).toHaveBeenCalledWith( [ { query: `SELECT * FROM "${CONFIG.user}"."${table}"`, values: [], }, ], items, nodeOptions, ); }); const getWhereClause = ( condition1: string = 'equal', val1: number = 2, condition2: string = 'equal', val2 = { department: 'Engineering', level: 3 }, ) => { const retObj: { values: any[] } = { values: [] }; let inval1: any = val1; if (condition1 === 'IS NULL' || condition1 === 'IS NOT NULL') { inval1 = undefined; } retObj.values.push({ column: 'ID', condition: condition1, value: inval1, }); retObj.values.push({ column: 'META_DATA', condition: condition2, value: val2, }); return retObj; }; const fixedValues = { bindVal: [ { type: oracleDBTypes.NUMBER, val: 2, }, { type: oracleDBTypes.DB_TYPE_JSON, val: { department: 'Engineering', level: 3, }, }, ], inOptions: { stmtBatching: 'independently', outputColumns: ['META_DATA', 'EMBEDDING'], }, }; const testCases = [ { ...fixedValues, whereClause: getWhereClause('>='), expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" >= :0 AND "META_DATA" = :1 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: { department: 'Engineering', level: 3, }, }, { ...fixedValues, whereClause: getWhereClause('!='), expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" != :0 AND "META_DATA" = :1 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: null, }, { ...fixedValues, whereClause: getWhereClause('equal', 2, 'LIKE'), expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" = :0 AND "META_DATA" LIKE :1 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: { department: 'Engineering', level: 3, }, }, { ...fixedValues, whereClause: getWhereClause('>', 2, 'LIKE'), expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" > :0 AND "META_DATA" LIKE :1 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: null, }, { ...fixedValues, whereClause: getWhereClause('<', 2, 'LIKE'), expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" < :0 AND "META_DATA" LIKE :1 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: null, }, { ...fixedValues, whereClause: getWhereClause('IS NULL'), bindVal: [fixedValues.bindVal[1]], expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" IS NULL AND "META_DATA" = :0 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: null, }, { ...fixedValues, whereClause: getWhereClause('IS NOT NULL'), bindVal: [fixedValues.bindVal[1]], expectedQuery: `SELECT "META_DATA","EMBEDDING" FROM "${CONFIG.user}"."${table}" WHERE "ID" IS NOT NULL AND "META_DATA" = :0 ORDER BY "ID" ASC FETCH FIRST 5 ROWS ONLY`, expectedResultMetadata: { department: 'Engineering', level: 3, }, }, ]; it.each(testCases)( 'should handle condition: %s', async ({ whereClause, expectedQuery, bindVal, inOptions, expectedResultMetadata }) => { const nodeParameters: IDataObject = { operation: 'select', schema: { __rl: true, mode: 'list', value: CONFIG.user, }, table: { __rl: true, value: table, mode: 'list', cachedResultName: table, }, limit: 5, where: whereClause, sort: { values: [ { column: 'ID', }, ], }, options: inOptions, }; const nodeOptions = nodeParameters.options as IDataObject; const mockThis = createMockExecuteFunction(nodeParameters); const runQueries = getRunQueriesFn(mockThis, pool); const result = await select.execute.call(mockThis, runQueries, items, nodeOptions, pool); expect(runQueries).toHaveBeenCalledWith( [ { query: expectedQuery, values: bindVal, }, ], items, nodeOptions, ); if (integratedTests) { if (expectedResultMetadata) { expect(result).toHaveLength(1); expect(result[0].json.META_DATA).toEqual(expectedResultMetadata); } else { expect(result[0].json).toEqual({ success: true }); } } }, ); }); describe('Test insert operation', () => { const joinDate = new Date(2024, 0, 1, 14, 30, 0); const insertId = 129; const baseItems = [ { ID: insertId, NAME: 'BOB', AGE: 39, SALARY: 4500, JOIN_DATE: joinDate, IS_ACTIVE: false, META_DATA: { address: 'BLR', pincode: 560126, }, EMBEDDING: [3, 4, 9], PICTURE: [20, 30, 45], PROFILE: [32, 34, 55], }, { ID: insertId + 1, NAME: 'Alice', AGE: 39, SALARY: 4500, JOIN_DATE: joinDate, IS_ACTIVE: false, META_DATA: { address: 'SVL', pincode: 560126, }, EMBEDDING: [3, 5, 9], PICTURE: [22, 30, 55], PROFILE: [2, 35, 56], }, ]; const expectedQuery = `INSERT INTO "${CONFIG.user}"."${table}" ("ID","NAME","AGE","SALARY","JOIN_DATE","IS_ACTIVE","META_DATA","EMBEDDING","PICTURE","PROFILE") VALUES (:0,:1,:2,:3,:4,:5,:6,:7,:8,:9) RETURNING "EMBEDDING", "ID" INTO :10, :11`; const expectedQueryAllOuts = `INSERT INTO "${CONFIG.user}"."${table}" ("ID","NAME","AGE","SALARY","JOIN_DATE","IS_ACTIVE","META_DATA","EMBEDDING","PICTURE","PROFILE") VALUES (:0,:1,:2,:3,:4,:5,:6,:7,:8,:9) RETURNING "AGE", "EMBEDDING", "ID", "IS_ACTIVE", "JOIN_DATE", "META_DATA", "NAME", "PICTURE", "PROFILE", "SALARY" INTO :10, :11, :12, :13, :14, :15, :16, :17, :18, :19`; const expectedValues = baseItems.map((v) => [ v.ID, v.NAME, v.AGE, v.SALARY, v.JOIN_DATE, v.IS_ACTIVE, v.META_DATA, v.EMBEDDING, Buffer.from(v.PICTURE), Buffer.from(v.PROFILE), ]); const expectedBindValuesForIndependent = baseItems.map((v) => [ { type: oracleDBTypes.NUMBER, val: v.ID }, { type: oracleDBTypes.DB_TYPE_VARCHAR, val: v.NAME }, { type: oracleDBTypes.NUMBER, val: v.AGE }, { type: oracleDBTypes.NUMBER, val: v.SALARY }, { type: oracleDBTypes.DB_TYPE_TIMESTAMP, val: v.JOIN_DATE }, { type: oracleDBTypes.DB_TYPE_BOOLEAN, val: v.IS_ACTIVE }, { type: oracleDBTypes.DB_TYPE_JSON, val: v.META_DATA }, { type: oracleDBTypes.DB_TYPE_VECTOR, val: v.EMBEDDING }, { type: oracleDBTypes.DB_TYPE_BLOB, val: Buffer.from(v.PICTURE) }, { type: oracleDBTypes.DB_TYPE_RAW, val: Buffer.from(v.PROFILE) }, { type: oracleDBTypes.DB_TYPE_VECTOR, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_NUMBER, dir: 3003 }, ]); const expectedBindValuesForIndependentAllOutputColumns = baseItems.map((v) => [ { type: oracleDBTypes.NUMBER, val: v.ID }, { type: oracleDBTypes.DB_TYPE_VARCHAR, val: v.NAME }, { type: oracleDBTypes.NUMBER, val: v.AGE }, { type: oracleDBTypes.NUMBER, val: v.SALARY }, { type: oracleDBTypes.DB_TYPE_TIMESTAMP, val: v.JOIN_DATE }, { type: oracleDBTypes.DB_TYPE_BOOLEAN, val: v.IS_ACTIVE }, { type: oracleDBTypes.DB_TYPE_JSON, val: v.META_DATA }, { type: oracleDBTypes.DB_TYPE_VECTOR, val: v.EMBEDDING }, { type: oracleDBTypes.DB_TYPE_BLOB, val: Buffer.from(v.PICTURE) }, { type: oracleDBTypes.DB_TYPE_RAW, val: Buffer.from(v.PROFILE) }, { type: oracleDBTypes.NUMBER, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_VECTOR, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_NUMBER, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_BOOLEAN, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_TIMESTAMP, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_JSON, dir: 3003 }, { type: oracleDBTypes.STRING, dir: 3003, maxSize: 100 }, { type: oracleDBTypes.DB_TYPE_BLOB, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_RAW, dir: 3003, maxSize: 2000 }, { type: oracleDBTypes.NUMBER, dir: 3003 }, ]); beforeAll(function () { queries = [ { query: expectedQuery, executeManyValues: expectedValues, outputColumns: ['EMBEDDING', 'ID'], }, ]; queriesIndependent = [ { query: expectedQuery, values: expectedBindValuesForIndependent[0], outputColumns: ['EMBEDDING', 'ID'], }, { query: expectedQuery, values: expectedBindValuesForIndependent[1], outputColumns: ['EMBEDDING', 'ID'], }, ]; }); const nodeParameters: IDataObject = { operation: 'insert', schema: { __rl: true, mode: 'list', value: CONFIG.user, }, table: { __rl: true, value: table, mode: 'list', cachedResultName: table, }, options: { stmtBatching: 'single', outputColumns: ['EMBEDDING', 'ID'], }, columns: { mappingMode: 'defineBelow', value: baseItems, }, }; let originalColumnsValue: any; beforeEach(() => { originalColumnsValue = structuredClone((nodeParameters.columns as any)?.value); }); afterEach(() => { jest.clearAllMocks(); continueOnFail = false; if (originalColumnsValue) { (nodeParameters.columns as any).value = originalColumnsValue; } queriesIndependent = [ { query: expectedQuery, values: expectedBindValuesForIndependent[0], outputColumns: ['EMBEDDING', 'ID'], }, { query: expectedQuery, values: expectedBindValuesForIndependent[1], outputColumns: ['EMBEDDING', 'ID'], }, ]; queries = [ { query: expectedQuery, executeManyValues: expectedValues, outputColumns: ['EMBEDDING', 'ID'], }, ]; }); it('should insert valid data (defineBelow) with select all outputcolumns independently batch mode', async () => { queriesIndependent[0].outputColumns = [ 'AGE', 'EMBEDDING', 'ID', 'IS_ACTIVE', 'JOIN_DATE', 'META_DATA', 'NAME', 'PICTURE', 'PROFILE', 'SALARY', ]; queriesIndependent[1].outputColumns = queriesIndependent[0].outputColumns; queriesIndependent[0].query = expectedQueryAllOuts; queriesIndependent[0].values = expectedBindValuesForIndependentAllOutputColumns[0]; queriesIndependent[1].query = expectedQueryAllOuts; queriesIndependent[1].values = expectedBindValuesForIndependentAllOutputColumns[1]; const selectAlloutputs: IDataObject = { ...(nodeParameters.options as Record), outputColumns: ['*'], stmtBatching: 'independently', }; const selectAlloutputsNodeParams: IDataObject = { ...(nodeParameters as Record), options: selectAlloutputs, }; await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: selectAlloutputsNodeParams, nodeOptions: selectAlloutputs, }); }); it('should insert valid data (defineBelow) with select all outputcolumns in single batch mode', async () => { queries[0].outputColumns = [ 'AGE', 'EMBEDDING', 'ID', 'IS_ACTIVE', 'JOIN_DATE', 'META_DATA', 'NAME', 'PICTURE', 'PROFILE', 'SALARY', ]; queries[0].query = expectedQueryAllOuts; const selectAlloutputs: IDataObject = { ...(nodeParameters.options as Record), outputColumns: ['*'], stmtBatching: 'single', }; const selectAlloutputsNodeParams: IDataObject = { ...(nodeParameters as Record), options: selectAlloutputs, }; await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: selectAlloutputsNodeParams, nodeOptions: selectAlloutputs, }); }); it('should insert valid data (defineBelow)', async () => { await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, }); }); it('should insert valid data (autoMap)', async () => { await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: nodeParameters.options as IDataObject, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (autoMap)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (autoMap)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should insert valid data (defineBelow) stmtBatching independent', async () => { await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, }); }); it('should insert valid data (autoMap) stmtBatching independent', async () => { await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow) stmtBatching independent', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow) stmtBatching independent', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /ORA-51803/, }); }); it('should insert valid data (defineBelow) stmtBatching transaction', async () => { await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, }); }); it('should insert valid data (autoMap) stmtBatching transaction', async () => { await runOperation({ items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow) stmtBatching transaction', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow) stmtBatching transaction', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, errorPattern: /ORA-51803/, }); }); }); describe('Test update operation', () => { const joinDate = new Date(2024, 0, 1, 14, 30, 0); const insertId = 1; const baseItems = [ { ID: insertId, NAME: 'BOB', AGE: 39, SALARY: 4500, JOIN_DATE: joinDate, IS_ACTIVE: false, META_DATA: { address: 'BLR', pincode: 560126, }, EMBEDDING: [3, 4, 9], }, { ID: insertId + 1, NAME: 'Alice', AGE: 39, SALARY: 4500, JOIN_DATE: joinDate, IS_ACTIVE: false, META_DATA: { address: 'SVL', pincode: 560126, }, EMBEDDING: [3, 5, 9], }, ]; const NonExistingItems = baseItems.map((item, index) => ({ ...item, ID: index + 100, // invalid ID })); const expectedQuery = `UPDATE "${CONFIG.user}"."${table}" SET "NAME"=:0,"AGE"=:1,"SALARY"=:2,"JOIN_DATE"=:3,"IS_ACTIVE"=:4,"META_DATA"=:5,"EMBEDDING"=:6 WHERE "ID"=:7 RETURNING "EMBEDDING", "ID" INTO :8, :9`; const expectedValues = baseItems.map((v) => [ v.NAME, v.AGE, v.SALARY, v.JOIN_DATE, v.IS_ACTIVE, v.META_DATA, v.EMBEDDING, v.ID, ]); const expectedBindValuesForIndependent = baseItems.map((v) => [ { type: oracleDBTypes.DB_TYPE_VARCHAR, val: v.NAME }, { type: oracleDBTypes.NUMBER, val: v.AGE }, { type: oracleDBTypes.NUMBER, val: v.SALARY }, { type: oracleDBTypes.DB_TYPE_TIMESTAMP, val: v.JOIN_DATE }, { type: oracleDBTypes.DB_TYPE_BOOLEAN, val: v.IS_ACTIVE }, { type: oracleDBTypes.DB_TYPE_JSON, val: v.META_DATA }, { type: oracleDBTypes.DB_TYPE_VECTOR, val: v.EMBEDDING }, { type: oracleDBTypes.NUMBER, val: v.ID }, { type: oracleDBTypes.DB_TYPE_VECTOR, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_NUMBER, dir: 3003 }, ]); beforeAll(function () { // initialize global variables to repective operation values. queries = [ { query: expectedQuery, executeManyValues: expectedValues, outputColumns: ['EMBEDDING', 'ID'], }, ]; queriesIndependent = [ { query: expectedQuery, values: expectedBindValuesForIndependent[0], outputColumns: ['EMBEDDING', 'ID'], }, { query: expectedQuery, values: expectedBindValuesForIndependent[1], outputColumns: ['EMBEDDING', 'ID'], }, ]; }); const nodeParameters: IDataObject = { operation: 'update', schema: { __rl: true, mode: 'list', value: CONFIG.user, }, table: { __rl: true, value: table, mode: 'list', cachedResultName: table, }, options: { stmtBatching: 'single', outputColumns: ['EMBEDDING', 'ID'], }, columns: { mappingMode: 'defineBelow', matchingColumns: ['ID'], }, }; let originalColumnsValue: any; beforeEach(() => { originalColumnsValue = structuredClone((nodeParameters.columns as any)?.value); }); afterEach(() => { jest.clearAllMocks(); continueOnFail = false; if (originalColumnsValue) { (nodeParameters.columns as any).value = originalColumnsValue; } }); it('should update valid data (defineBelow)', async () => { await runOperation({ operation: update, items: baseItems, nodePars: nodeParameters, continueOnFailValue: true, nodeOptions: nodeParameters.options as IDataObject, }); }); it('should update valid data (autoMap)', async () => { await runOperation({ operation: update, items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: nodeParameters.options as IDataObject, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (autoMap)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: true, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (autoMap)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: false, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should update valid data (defineBelow) stmtBatching transaction', async () => { await runOperation({ operation: update, items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, }); }); it('should throw error on update for non existing row (defineBelow) stmtBatching transaction', async () => { await runOperation({ operation: update, items: NonExistingItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, errorPattern: /The row you are trying to update doesn't exist/, }); }); it('should insert valid data (autoMap) stmtBatching transaction', async () => { await runOperation({ operation: update, items: baseItems, continueOnFailValue: true, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow) stmtBatching transaction', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow) stmtBatching transaction', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, errorPattern: /ORA-51803/, }); }); it('should update valid data (defineBelow) stmtBatching transaction', async () => { await runOperation({ operation: update, items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'transaction', }, }); }); it('should throw error on update for non existing row (defineBelow) stmtBatching independent', async () => { await runOperation({ operation: update, items: NonExistingItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /The row you are trying to update doesn't exist/, }); }); it('should insert valid data (autoMap) stmtBatching independent', async () => { await runOperation({ operation: update, items: baseItems, continueOnFailValue: true, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow) stmtBatching independent', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow) stmtBatching independent', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: update, items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /ORA-51803/, }); }); }); describe('Test upsert operation', () => { const joinDate = new Date(2024, 0, 1, 14, 30, 0); const insertId = 1; const baseItems = [ { ID: insertId, NAME: 'BOB', AGE: 39, SALARY: 4500, JOIN_DATE: joinDate, IS_ACTIVE: false, META_DATA: { address: 'BLR', pincode: 560126, }, EMBEDDING: [3, 4, 9], }, { ID: insertId + 1, NAME: 'Alice', AGE: 39, SALARY: 4500, JOIN_DATE: joinDate, IS_ACTIVE: false, META_DATA: { address: 'SVL', pincode: 560126, }, EMBEDDING: [3, 5, 9], }, ]; const NonExistingItems = baseItems.map((item, index) => ({ ...item, ID: index + 101, // invalid ID })); const expectedQuery = `MERGE INTO "${CONFIG.user}"."${table}" t USING (SELECT :0 as "ID" FROM dual) s ON (t."ID" = s."ID") WHEN MATCHED THEN UPDATE SET t."NAME" = :1, t."AGE" = :2, t."SALARY" = :3, t."JOIN_DATE" = :4, t."IS_ACTIVE" = :5, t."META_DATA" = :6, t."EMBEDDING" = :7 WHEN NOT MATCHED THEN INSERT ("ID", "NAME", "AGE", "SALARY", "JOIN_DATE", "IS_ACTIVE", "META_DATA", "EMBEDDING") VALUES (:8, :9, :10, :11, :12, :13, :14, :15) RETURNING "EMBEDDING", "ID" INTO :16, :17`; const expectedValues = baseItems.map((v) => [ v.ID, v.NAME, v.AGE, v.SALARY, v.JOIN_DATE, v.IS_ACTIVE, v.META_DATA, v.EMBEDDING, v.ID, v.NAME, v.AGE, v.SALARY, v.JOIN_DATE, v.IS_ACTIVE, v.META_DATA, v.EMBEDDING, ]); const expectedBindValuesForIndependent = baseItems.map((v) => [ { type: oracleDBTypes.NUMBER, val: v.ID }, { type: oracleDBTypes.DB_TYPE_VARCHAR, val: v.NAME }, { type: oracleDBTypes.NUMBER, val: v.AGE }, { type: oracleDBTypes.NUMBER, val: v.SALARY }, { type: oracleDBTypes.DB_TYPE_TIMESTAMP, val: v.JOIN_DATE }, { type: oracleDBTypes.DB_TYPE_BOOLEAN, val: v.IS_ACTIVE }, { type: oracleDBTypes.DB_TYPE_JSON, val: v.META_DATA }, { type: oracleDBTypes.DB_TYPE_VECTOR, val: v.EMBEDDING }, { type: oracleDBTypes.NUMBER, val: v.ID }, { type: oracleDBTypes.DB_TYPE_VARCHAR, val: v.NAME }, { type: oracleDBTypes.NUMBER, val: v.AGE }, { type: oracleDBTypes.NUMBER, val: v.SALARY }, { type: oracleDBTypes.DB_TYPE_TIMESTAMP, val: v.JOIN_DATE }, { type: oracleDBTypes.DB_TYPE_BOOLEAN, val: v.IS_ACTIVE }, { type: oracleDBTypes.DB_TYPE_JSON, val: v.META_DATA }, { type: oracleDBTypes.DB_TYPE_VECTOR, val: v.EMBEDDING }, { type: oracleDBTypes.DB_TYPE_VECTOR, dir: 3003 }, { type: oracleDBTypes.DB_TYPE_NUMBER, dir: 3003 }, ]); const expectedNonExistentBindValuesForIndependent = expectedBindValuesForIndependent.map( (bindValues) => { const modifiedValues = [...bindValues]; // Modify only bindValues.ID at index 0 and 8 if (typeof modifiedValues[0]?.val === 'number') { modifiedValues[0] = { ...modifiedValues[0], val: modifiedValues[0].val + 100, }; } if (typeof modifiedValues[8]?.val === 'number') { modifiedValues[8] = { ...modifiedValues[8], val: modifiedValues[8].val + 100, }; } return { values: modifiedValues, }; }, ); const expectedNonExistentBindValues = structuredClone(expectedValues); for (let i = 0; i < expectedNonExistentBindValues.length; i++) { if (Array.isArray(expectedValues[i]) && Array.isArray(expectedNonExistentBindValues[i])) { const val0 = expectedValues[i][0]; const val8 = expectedValues[i][8]; if (typeof val0 === 'number') { expectedNonExistentBindValues[i][0] = val0 + 100; } if (typeof val8 === 'number') { expectedNonExistentBindValues[i][8] = val8 + 100; } } } beforeAll(function () { // initialize global variables to respective operation values. queries = [ { query: expectedQuery, executeManyValues: expectedValues, outputColumns: ['EMBEDDING', 'ID'], }, ]; queriesIndependent = [ { query: expectedQuery, values: expectedBindValuesForIndependent[0], outputColumns: ['EMBEDDING', 'ID'], }, { query: expectedQuery, values: expectedBindValuesForIndependent[1], outputColumns: ['EMBEDDING', 'ID'], }, ]; }); const nodeParameters: IDataObject = { operation: 'upsert', schema: { __rl: true, mode: 'list', value: CONFIG.user, }, table: { __rl: true, value: table, mode: 'list', cachedResultName: table, }, options: { stmtBatching: 'single', operation: 'upsert', outputColumns: ['EMBEDDING', 'ID'], }, columns: { mappingMode: 'defineBelow', matchingColumns: ['ID'], }, }; let originalColumnsValue: any; beforeEach(() => { originalColumnsValue = structuredClone((nodeParameters.columns as any)?.value); }); afterEach(() => { jest.clearAllMocks(); continueOnFail = false; // restore variables modified in tests. if (originalColumnsValue) { (nodeParameters.columns as any).value = originalColumnsValue; } queriesIndependent[0].values = expectedBindValuesForIndependent[0]; queriesIndependent[1].values = expectedBindValuesForIndependent[1]; queries[0].executeManyValues = expectedValues; }); it('should upsert valid data (defineBelow)', async () => { await runOperation({ operation: upsert, items: baseItems, nodePars: nodeParameters, continueOnFailValue: true, nodeOptions: nodeParameters.options as IDataObject, }); }); it('should upsert valid data (autoMap)', async () => { await runOperation({ operation: upsert, items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, mappingMode: 'autoMapInputData', nodeOptions: nodeParameters.options as IDataObject, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: upsert, items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: upsert, items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (autoMap)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: upsert, items: modifiedItems, continueOnFailValue: true, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (autoMap)', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: upsert, items: modifiedItems, continueOnFailValue: false, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, errorPattern: /ORA-51803/, }); }); it('should update valid data (defineBelow) stmtBatching independent', async () => { await runOperation({ operation: upsert, items: baseItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, }); }); it('should do insert on upsert for non existing row (defineBelow)', async () => { queries[0].executeManyValues = expectedNonExistentBindValues; await runOperation({ operation: upsert, items: NonExistingItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, }); }); it('should do insert on upsert for non existing row (autoMap)', async () => { queries[0].executeManyValues = expectedNonExistentBindValues; await runOperation({ operation: upsert, items: NonExistingItems, continueOnFailValue: false, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: nodeParameters.options as IDataObject, }); }); it('should do insert on upsert for non existing row (defineBelow) stmtBatching independent', async () => { queriesIndependent[0].values = expectedNonExistentBindValuesForIndependent[0].values; queriesIndependent[1].values = expectedNonExistentBindValuesForIndependent[1].values; await runOperation({ operation: upsert, items: NonExistingItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, }); }); it('should insert valid data (autoMap) stmtBatching independent', async () => { await runOperation({ operation: upsert, items: baseItems, continueOnFailValue: true, mappingMode: 'autoMapInputData', nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, }); }); it('should handle invalid EMBEDDING with continueOnFail=true (defineBelow) stmtBatching independent', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: upsert, items: modifiedItems, continueOnFailValue: true, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /ORA-51803/, }); }); it('should handle invalid EMBEDDING with continueOnFail=false (defineBelow) stmtBatching independent', async () => { const modifiedItems = structuredClone(baseItems); modifiedItems[1].EMBEDDING = [2, 3, 4, 6]; await runOperation({ operation: upsert, items: modifiedItems, continueOnFailValue: false, nodePars: nodeParameters, nodeOptions: { ...((nodeParameters.options as IDataObject) ?? {}), stmtBatching: 'independently', }, errorPattern: /ORA-51803/, }); }); }); });