Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/nodes-base/credentials/OracleDBApi.credentials.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
import oracledb from 'oracledb';

const privilegeKeys = [
'SYSASM',
'SYSBACKUP',
'SYSDBA',
'SYSDG',
'SYSKM',
'SYSOPER',
'SYSPRELIM',
'SYSRAC',
];

const privilegeOptions = privilegeKeys.map((key) => ({
name: key,
value: (oracledb as any)[key],
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prefer Typeguards over Type casting

Casting the oracledb module to any to access privilege constants suppresses its declared typings and violates the "Prefer Typeguards over Type casting" rule; please rely on typed keys (e.g., via satisfies/as const) so the lookup stays type-safe.

Prompt for AI agents
Address the following comment on packages/nodes-base/credentials/OracleDBApi.credentials.ts at line 17:

<comment>Casting the `oracledb` module to `any` to access privilege constants suppresses its declared typings and violates the &quot;Prefer Typeguards over Type casting&quot; rule; please rely on typed keys (e.g., via `satisfies`/`as const`) so the lookup stays type-safe.</comment>

<file context>
@@ -1,4 +1,21 @@
+
+const privilegeOptions = privilegeKeys.map((key) =&gt; ({
+	name: key,
+	value: (oracledb as any)[key],
+}));
 
</file context>
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The map is static and keys are always exposed by oracledb module.

}));

export class OracleDBApi implements ICredentialType {
name = 'oracleDBApi';
Expand Down Expand Up @@ -30,6 +47,14 @@ export class OracleDBApi implements ICredentialType {
default: 'localhost/orcl',
description: 'The Oracle database instance to connect to',
},
{
displayName: 'Privilege',
name: 'privilege',
type: 'options',
description: 'The privilege to use when connecting to the database',
default: undefined,
options: privilegeOptions,
},
{
displayName: 'Use Optional Oracle Client Libraries',
name: 'useThickMode',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ export async function execute(
): Promise<INodeExecutionData[]> {
const queries: QueryWithValues[] = [];

const conn = await pool.getConnection();
const isCDBSupported = conn.oracleServerVersion >= 1200000000;
await conn.close();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Connection leak from unclosed database connection

Connection resource leak: The database connection is acquired but not protected by try-finally. If an error occurs after getConnection() on line 90 but before close() on line 92 (e.g., if accessing conn.oracleServerVersion throws an error), the connection will not be closed and will leak. This should be wrapped in a try-finally block to ensure the connection is always closed.

Fix in Cursor Fix in Web

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oracleServerVersion is a public exported attribute exposed from oracledb module which is always available. It should not cause an exception.


for (let i = 0; i < items.length; i++) {
const schema = this.getNodeParameter('schema', i, undefined, {
extractValue: true,
Expand All @@ -102,26 +106,46 @@ export async function execute(
const outputColumns = this.getNodeParameter('options.outputColumns', i, ['*']) as string[];

let query = '';
if (outputColumns.includes('*')) {
query = `SELECT * FROM ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;
} else {
const quotedColumns = outputColumns.map(quoteSqlIdentifier).join(',');
query = `SELECT ${quotedColumns} FROM ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;
}
let innerQuery = outputColumns.includes('*')
? `SELECT * FROM ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`
: `SELECT ${outputColumns.map(quoteSqlIdentifier).join(',')} FROM ${quoteSqlIdentifier(schema)}.${quoteSqlIdentifier(table)}`;

// Add WHERE clause
const whereClauses =
((this.getNodeParameter('where', i, []) as IDataObject).values as WhereClause[]) || [];
const combineConditions = this.getNodeParameter('combineConditions', i, 'AND') as string;
[query, values] = addWhereClauses(query, whereClauses, combineConditions, columnMetaDataObject);
[innerQuery, values] = addWhereClauses(
innerQuery,
whereClauses,
combineConditions,
columnMetaDataObject,
);

// Add ORDER BY if needed
const sortRules =
((this.getNodeParameter('sort', i, []) as IDataObject).values as SortRule[]) || [];
query = addSortRules(query, sortRules);
innerQuery = addSortRules(innerQuery, sortRules);

// Handle LIMIT / pagination
const returnAll = this.getNodeParameter('returnAll', i, false);
if (!returnAll) {
const limit = this.getNodeParameter('limit', i, 50);
query += ` FETCH FIRST ${limit} ROWS ONLY`;

if (isCDBSupported) {
// Oracle 12c+ (FETCH FIRST)
query += `${innerQuery} FETCH FIRST ${limit} ROWS ONLY`;
} else {
if (sortRules.length > 0 || whereClauses.length > 0) {
// Wrap inner query to preserve WHERE + ORDER BY
query = `SELECT * FROM (${innerQuery}) WHERE ROWNUM <= ${limit}`;
} else {
// No ORDER BY or WHERE: safe to append ROWNUM inline
query = `${innerQuery} WHERE ROWNUM <= ${limit}`;
}
}
} else {
// return all: no limit
query = innerQuery;
}

const queryWithValues = { query, values };
Expand Down
14 changes: 14 additions & 0 deletions packages/nodes-base/nodes/Oracle/Sql/helpers/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export type OracleDBNodeCredentials = {
poolMin: number;
poolMax: number;
poolIncrement: number;
privilege?: number;
sslServerCertDN?: string;
sslServerDNMatch?: boolean;
sslAllowWeakDNMatch?: boolean;
Expand Down Expand Up @@ -122,3 +123,16 @@ export type ExecuteOpBindParam =
values: number[];
};
});

// Definition of row returned for column information.
export interface TableColumnRow {
COLUMN_NAME: string;
DATA_TYPE: string;
DATA_LENGTH: number;
CHAR_LENGTH: number;
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please type CHAR_LENGTH as number | null since ALL_TAB_COLUMNS.CHAR_LENGTH is NULL for non-character columns; keeping it as number can lead callers to assume it’s always numeric and skip null checks.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Oracle/Sql/helpers/interfaces.ts at line 132:

<comment>Please type CHAR_LENGTH as number | null since ALL_TAB_COLUMNS.CHAR_LENGTH is NULL for non-character columns; keeping it as number can lead callers to assume it’s always numeric and skip null checks.</comment>

<file context>
@@ -122,3 +123,16 @@ export type ExecuteOpBindParam =
+	COLUMN_NAME: string;
+	DATA_TYPE: string;
+	DATA_LENGTH: number;
+	CHAR_LENGTH: number;
+	DEFAULT_LENGTH: number | null;
+	NULLABLE: &#39;Y&#39; | &#39;N&#39;;
</file context>
Suggested change
CHAR_LENGTH: number;
CHAR_LENGTH: number | null;
Fix with Cubic

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CHAR_LENGTH will come as 0 if its not valid like for number type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

DEFAULT_LENGTH: number | null;
NULLABLE: 'Y' | 'N';
IDENTITY_COLUMN?: 'YES' | 'NO'; // only present in 12c+
HAS_DEFAULT: 'YES' | 'NO';
CONSTRAINT_TYPES?: string | null;
}
118 changes: 98 additions & 20 deletions packages/nodes-base/nodes/Oracle/Sql/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
SortRule,
ColumnMap,
OracleDBNodeOptions,
TableColumnRow,
} from './interfaces';

const n8nTypetoDBType: { [key: string]: oracledb.DbType } = {
Expand Down Expand Up @@ -240,13 +241,17 @@ export async function getColumnMetaData(
let conn: oracledb.Connection | undefined;
try {
conn = await pool.getConnection();
const sql = `WITH constraint_info AS (

const isCDBSupported = conn.oracleServerVersion >= 1200000000;

const sql = isCDBSupported
? `
WITH constraint_info AS (
SELECT
acc.owner,
acc.table_name,
acc.column_name,
LISTAGG(ac.constraint_type, ',')
WITHIN GROUP (ORDER BY ac.constraint_type) AS constraint_types
LISTAGG(ac.constraint_type, ',') WITHIN GROUP (ORDER BY ac.constraint_type) AS constraint_types
FROM all_cons_columns acc
JOIN all_constraints ac
ON acc.constraint_name = ac.constraint_name
Expand All @@ -264,39 +269,74 @@ SELECT
CASE WHEN atc.DATA_DEFAULT IS NOT NULL THEN 'YES' ELSE 'NO' END AS HAS_DEFAULT,
ci.constraint_types
FROM all_tab_columns atc
LEFT JOIN constraint_info ci
ON atc.owner = ci.owner
AND atc.table_name = ci.table_name
AND atc.column_name = ci.column_name
WHERE atc.owner = :schema_name
AND atc.table_name = :table_name
ORDER BY atc.COLUMN_NAME`
: `
WITH constraint_info AS (
SELECT
acc.owner,
acc.table_name,
acc.column_name,
RTRIM(
XMLAGG(
XMLELEMENT(e, ac.constraint_type || ',')
ORDER BY ac.constraint_type
).EXTRACT('//text()').getClobVal(),
','
) AS constraint_types
FROM all_cons_columns acc
JOIN all_constraints ac
ON acc.constraint_name = ac.constraint_name
AND acc.owner = ac.owner
GROUP BY acc.owner, acc.table_name, acc.column_name
)
SELECT
atc.COLUMN_NAME,
atc.DATA_TYPE,
atc.DATA_LENGTH,
atc.CHAR_LENGTH,
atc.DEFAULT_LENGTH,
atc.NULLABLE,
CASE WHEN atc.DATA_DEFAULT IS NOT NULL THEN 'YES' ELSE 'NO' END AS HAS_DEFAULT,
ci.constraint_types
FROM all_tab_columns atc
LEFT JOIN constraint_info ci
ON atc.owner = ci.owner
AND atc.table_name = ci.table_name
AND atc.column_name = ci.column_name
WHERE atc.owner = :schema_name
AND atc.table_name = :table_name
ORDER BY atc.COLUMN_NAME`;
const result = await conn.execute(

const result = await conn.execute<TableColumnRow>(
sql,
{ table_name: table, schema_name: schema },
{ outFormat: oracledb.OUT_FORMAT_OBJECT },
);

// If schema is not available, throw error.
if (!result.rows || result.rows.length === 0) {
const rows = result.rows ?? [];
if (rows.length === 0) {
throw new NodeOperationError(node, 'Schema Information does not exist for selected table', {
itemIndex: index,
});
}

return (
result.rows?.map((row: any) => ({
columnName: row.COLUMN_NAME,
isGenerated: row.IDENTITY_COLUMN === 'YES' ? 'ALWAYS' : 'NEVER',
columnDefault: row.HAS_DEFAULT,
dataType: row.DATA_TYPE,
isNullable: row.NULLABLE === 'Y',
maxSize: row.DATA_LENGTH,
})) ?? []
);
return rows.map((row) => ({
columnName: row.COLUMN_NAME,
isGenerated: isCDBSupported && row.IDENTITY_COLUMN === 'YES' ? 'ALWAYS' : 'NEVER',
columnDefault: row.HAS_DEFAULT,
dataType: row.DATA_TYPE,
isNullable: row.NULLABLE === 'Y',
maxSize: row.DATA_LENGTH,
}));
} finally {
if (conn) {
await conn.close(); // Ensure connection is closed
await conn.close();
}
}
}
Expand Down Expand Up @@ -329,10 +369,20 @@ function normalizeOutBinds(
stmtBatching: string,
outputColumns: string[],
): Array<Record<string, any>> {
if (!Array.isArray(outBinds)) return [];

const rows: Array<Record<string, any>> = [];

if (!Array.isArray(outBinds)) {
// For execute operation mode, we get outBinds as object and
// array for other insert, update, upsert operations.
const row: Record<string, any> = {};
for (const [key, val] of Object.entries(outBinds as Record<string, unknown>)) {
// If val is expected to be an array, safely extract the first element
row[key] = Array.isArray(val) ? val[0] : val;
}
rows.push(row);
return rows;
}

// executeMany case outBinds-> [ [[col1Row1Val], [col2Row1Val]], [[col1Row2Val], [col2Row2Val]], ...]
if (stmtBatching === 'single') {
for (const batch of outBinds as any[][]) {
Expand Down Expand Up @@ -772,6 +822,17 @@ function generateBindVariablesList(
return query.replace(regex, generatedSqlString);
}

function isSerializedBuffer(val: unknown): val is { type: 'Buffer'; data: number[] } {
return (
typeof val === 'object' &&
val !== null &&
'type' in val &&
'data' in val &&
(val as any).type === 'Buffer' &&
Array.isArray((val as any).data)
);
}

export function getBindParameters(
query: string,
parameterList: ExecuteOpBindParam[],
Expand All @@ -795,7 +856,24 @@ export function getBindParameters(
break;
case 'blob':
bindVal = item.valueBlob;
break;

// Allow null or undefined to represent SQL NULL BLOB values
if (bindVal === null) {
break;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Standardize BLOB Null Value Checks

The comment states both null and undefined should represent SQL NULL BLOB values, but the code only checks for null. When bindVal is undefined, it bypasses the null check and eventually throws an error. The condition should be if (bindVal === null || bindVal === undefined) to match the comment's stated intent and handle optional BLOB values consistently.

Fix in Cursor Fix in Web


if (Buffer.isBuffer(bindVal)) {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Nov 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null BLOB bind values now trigger a UserError. Allow nulls to pass through so existing workflows can still write NULL into BLOB columns.

Prompt for AI agents
Address the following comment on packages/nodes-base/nodes/Oracle/Sql/helpers/utils.ts at line 848:

<comment>Null BLOB bind values now trigger a UserError. Allow nulls to pass through so existing workflows can still write NULL into BLOB columns.</comment>

<file context>
@@ -795,7 +845,23 @@ export function getBindParameters(
 				case &#39;blob&#39;:
 					bindVal = item.valueBlob;
-					break;
+					if (Buffer.isBuffer(bindVal)) {
+						break;
+					}
</file context>

✅ Addressed in 9a7ddfb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added NULL check

break;
}

// Serialized form: { type: 'Buffer', data: [...] }
if (isSerializedBuffer(bindVal)) {
bindVal = Buffer.from((bindVal as any).data);
break;
}
throw new UserError(
'BLOB data must be a valid Buffer or \'{ type: "Buffer", data: [...] }\'',
);
case 'date': {
const val = item.valueDate;
if (typeof val === 'string') {
Expand Down
Loading