Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
2581 lines
74 KiB
TypeScript
2581 lines
74 KiB
TypeScript
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<any>;
|
||
|
||
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<ReturnType<RunQueriesFn>, Parameters<RunQueriesFn>> {
|
||
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<RunQueriesFn>) => {
|
||
return await realRunQueries(...args);
|
||
});
|
||
}
|
||
|
||
// Default: return mock function with correct signature
|
||
return jest.fn<ReturnType<RunQueriesFn>, Parameters<RunQueriesFn>>();
|
||
}
|
||
|
||
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<string, any> | undefined) {
|
||
if (!params) return params;
|
||
|
||
const normalized: Record<string, any> = {};
|
||
const groupMap: Record<string, any[]> = {};
|
||
|
||
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<ReturnType<RunQueriesFn>, Parameters<RunQueriesFn>>,
|
||
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<OracleDBNodeCredentials>('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<string, { val: any }>)) {
|
||
if (key === 'ID') {
|
||
expect(bind.val).toBe(input.expectedId);
|
||
} else {
|
||
const original = (input.items[0].json as Record<string, any>)[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<string, any>)[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<string, any>),
|
||
outputColumns: ['*'],
|
||
stmtBatching: 'independently',
|
||
};
|
||
|
||
const selectAlloutputsNodeParams: IDataObject = {
|
||
...(nodeParameters as Record<string, any>),
|
||
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<string, any>),
|
||
outputColumns: ['*'],
|
||
stmtBatching: 'single',
|
||
};
|
||
|
||
const selectAlloutputsNodeParams: IDataObject = {
|
||
...(nodeParameters as Record<string, any>),
|
||
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/,
|
||
});
|
||
});
|
||
});
|
||
});
|