fix(oracle): bind out parameters when executing procedures

This commit is contained in:
Jeff
2026-10-15 10:33:56 +08:00
committed by GitHub
parent ab1cbf1a0a
commit 131db14d62
2 changed files with 220 additions and 1 deletions
@@ -161,6 +161,143 @@ describe("SQL Server routine execution SQL", () => {
});
});
describe("Oracle routine execution SQL", () => {
it("declares OUT variables, passes them to the call, and prints them", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "oracle",
schema: "APP",
routineName: "GET_NAME",
parameters: [
{ name: "P_ID", dataType: "NUMBER", mode: "IN", ordinal: 1, value: "1" },
{ name: "P_NAME", dataType: "VARCHAR2", mode: "OUT", ordinal: 2, value: "ignored" },
],
});
expect(sql).toBe(["DECLARE", " dbx_output_2 VARCHAR2(32767);", "BEGIN", ' "APP"."GET_NAME"(1, dbx_output_2);', " DBMS_OUTPUT.PUT_LINE('P_NAME = ' || SUBSTR(dbx_output_2, 1, 4000));", "END;"].join("\n"));
});
it("initializes IN OUT variables from the entered value", () => {
const parameters = routineParametersFromResult(queryResult(["name", "data_type", "mode", "ordinal", "has_default"], [["P_COUNTER", "NUMBER", "IN/OUT", 1, "N"]]), "oracle");
expect(
buildProcedureExecutionSqlFromValues({
databaseType: "oracle",
schema: "APP",
routineName: "BUMP",
parameters: parameters.map((parameter) => ({ ...parameter, value: "5" })),
}),
).toBe(["DECLARE", " dbx_output_1 NUMBER := 5;", "BEGIN", ' "APP"."BUMP"(dbx_output_1);', " DBMS_OUTPUT.PUT_LINE('P_COUNTER = ' || dbx_output_1);", "END;"].join("\n"));
});
it("binds several OUT parameters of different types", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "oracle",
schema: "APP",
routineName: "MULTI",
parameters: [
{ name: "P_ID", dataType: "NUMBER", mode: "IN", ordinal: 1, value: "3" },
{ name: "O_NUM", dataType: "NUMBER", mode: "OUT", ordinal: 2, value: "" },
{ name: "O_TEXT", dataType: "VARCHAR2", mode: "OUT", ordinal: 3, value: "" },
{ name: "O_DATE", dataType: "DATE", mode: "OUT", ordinal: 4, value: "" },
],
});
expect(sql).toBe(
[
"DECLARE",
" dbx_output_2 NUMBER;",
" dbx_output_3 VARCHAR2(32767);",
" dbx_output_4 DATE;",
"BEGIN",
' "APP"."MULTI"(3, dbx_output_2, dbx_output_3, dbx_output_4);',
" DBMS_OUTPUT.PUT_LINE('O_NUM = ' || dbx_output_2);",
" DBMS_OUTPUT.PUT_LINE('O_TEXT = ' || SUBSTR(dbx_output_3, 1, 4000));",
" DBMS_OUTPUT.PUT_LINE('O_DATE = ' || dbx_output_4);",
"END;",
].join("\n"),
);
});
it("switches to named arguments when an omitted default precedes an OUT parameter", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "oracle",
schema: "APP",
routineName: "WITH_DEFAULT",
parameters: [
{ name: "P_ID", dataType: "NUMBER", mode: "IN", ordinal: 1, value: "1" },
{ name: "P_FLAG", dataType: "VARCHAR2", mode: "IN", ordinal: 2, value: "", hasDefault: true, useDefault: true },
{ name: "O_MSG", dataType: "VARCHAR2", mode: "OUT", ordinal: 3, value: "" },
],
});
expect(sql).toContain(' "APP"."WITH_DEFAULT"("P_ID" => 1, "O_MSG" => dbx_output_3);');
});
it("sizes unconstrained types and skips values that cannot be printed", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "oracle",
schema: "APP",
routineName: "TYPES",
parameters: [
{ name: "O_CHAR", dataType: "CHAR", mode: "OUT", ordinal: 1, value: "" },
{ name: "O_NCHAR", dataType: "NCHAR", mode: "OUT", ordinal: 2, value: "" },
{ name: "O_NVC", dataType: "NVARCHAR2", mode: "OUT", ordinal: 3, value: "" },
{ name: "O_RAW", dataType: "RAW", mode: "OUT", ordinal: 4, value: "" },
{ name: "O_CUR", dataType: "REF CURSOR", mode: "OUT", ordinal: 5, value: "" },
{ name: "O_BOOL", dataType: "PL/SQL BOOLEAN", mode: "OUT", ordinal: 6, value: "" },
{ name: "O_BLOB", dataType: "BLOB", mode: "OUT", ordinal: 7, value: "" },
{ name: "O_TS", dataType: "TIMESTAMP WITH TIME ZONE", mode: "OUT", ordinal: 8, value: "" },
],
});
expect(sql).toBe(
[
"DECLARE",
" dbx_output_1 CHAR(2000);",
" dbx_output_2 NCHAR(1000);",
" dbx_output_3 NVARCHAR2(10922);",
" dbx_output_4 RAW(32767);",
" dbx_output_5 SYS_REFCURSOR;",
" dbx_output_6 BOOLEAN;",
" dbx_output_7 BLOB;",
" dbx_output_8 TIMESTAMP WITH TIME ZONE;",
"BEGIN",
' "APP"."TYPES"(dbx_output_1, dbx_output_2, dbx_output_3, dbx_output_4, dbx_output_5, dbx_output_6, dbx_output_7, dbx_output_8);',
" DBMS_OUTPUT.PUT_LINE('O_CHAR = ' || SUBSTR(dbx_output_1, 1, 4000));",
" DBMS_OUTPUT.PUT_LINE('O_NCHAR = ' || SUBSTR(dbx_output_2, 1, 4000));",
" DBMS_OUTPUT.PUT_LINE('O_NVC = ' || SUBSTR(dbx_output_3, 1, 4000));",
" DBMS_OUTPUT.PUT_LINE('O_TS = ' || dbx_output_8);",
"END;",
].join("\n"),
);
});
it("keeps input-only procedure calls unchanged", () => {
expect(
buildProcedureExecutionSqlFromValues({
databaseType: "oracle",
schema: "APP",
routineName: "SAVE_VALUE",
parameters: [
{ name: "P_ID", dataType: "NUMBER", mode: "IN", ordinal: 1, value: "1" },
{ name: "P_NAME", dataType: "VARCHAR2", mode: "IN", ordinal: 2, value: "O'Reilly" },
],
}),
).toBe("BEGIN\n \"APP\".\"SAVE_VALUE\"(1, 'O''Reilly');\nEND;");
});
it("uses the same output bindings for OceanBase Oracle mode", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "oceanbase-oracle",
schema: "APP",
routineName: "GET_NAME",
parameters: [{ name: "P_NAME", dataType: "VARCHAR2", mode: "OUT", ordinal: 1, value: "" }],
});
expect(sql).toContain(' "APP"."GET_NAME"(dbx_output_1);');
});
});
describe("XuguDB routine parameter metadata", () => {
it("parses modes, nested types, defaults, comments, and quoted identifiers", () => {
const metadata = xuguRoutineMetadataFromDefinition(`
@@ -51,9 +51,12 @@ export function buildProcedureExecutionSqlFromValues(options: BuildRoutineExecut
if (options.databaseType === "xugu") {
return buildXuguProcedureExecutionSql(routine, sortedParameters);
}
if (options.databaseType === "oracle" || options.databaseType === "oceanbase-oracle") {
return buildOracleProcedureExecutionSql(options.databaseType, routine, sortedParameters);
}
const values = sortedParameters.filter((parameter) => shouldIncludeParameter(parameter));
const useNamedArguments = shouldUseNamedArguments(options.databaseType, sortedParameters);
if (options.databaseType === "oracle" || options.databaseType === "dameng" || options.databaseType === "oceanbase-oracle") {
if (options.databaseType === "dameng") {
return `BEGIN\n ${routine}(${values.map((parameter) => routineArgumentSql(options.databaseType, parameter, useNamedArguments)).join(", ")});\nEND;`;
}
if (options.databaseType === "databend") {
@@ -171,6 +174,85 @@ function buildSqlServerProcedureExecutionSql(routine: string, sortedParameters:
return statements.join("\n");
}
function buildOracleProcedureExecutionSql(databaseType: "oracle" | "oceanbase-oracle", routine: string, sortedParameters: RoutineParameterValue[]): string {
const outputBindings = new Map<RoutineParameterValue, string>();
const declarations: string[] = [];
const printStatements: string[] = [];
sortedParameters.forEach((parameter, index) => {
if (!returnsRoutineOutput(parameter)) return;
const variableName = `dbx_output_${index + 1}`;
const initialValue = parameter.mode === "INOUT" ? ` := ${routineParameterSqlValue(databaseType, parameter)}` : "";
declarations.push(` ${variableName} ${oracleVariableType(parameter.dataType)}${initialValue};`);
outputBindings.set(parameter, variableName);
const printable = oraclePrintableValue(parameter.dataType, variableName);
if (printable) {
printStatements.push(` DBMS_OUTPUT.PUT_LINE(${quoteSqlString(`${parameter.name || `output_${index + 1}`} = `)} || ${printable});`);
}
});
const useNamedArguments = shouldUseOracleNamedArguments(sortedParameters, outputBindings);
const args = sortedParameters.flatMap((parameter) => {
const variableName = outputBindings.get(parameter);
if (variableName) {
return [useNamedArguments ? `${quoteTableIdentifier(databaseType, parameter.name)} => ${variableName}` : variableName];
}
if (!shouldIncludeParameter(parameter)) return [];
return [routineArgumentSql(databaseType, parameter, useNamedArguments)];
});
const call = ` ${routine}(${args.join(", ")});`;
if (outputBindings.size === 0) return `BEGIN\n${call}\nEND;`;
return ["DECLARE", ...declarations, "BEGIN", call, ...printStatements, "END;"].join("\n");
}
function shouldUseOracleNamedArguments(sortedParameters: RoutineParameterValue[], outputBindings: Map<RoutineParameterValue, string>): boolean {
let omittedDefault = false;
for (const parameter of sortedParameters) {
if (!outputBindings.has(parameter) && parameter.useDefault && parameter.hasDefault && acceptsRoutineInput(parameter)) {
omittedDefault = true;
continue;
}
if (omittedDefault && (outputBindings.has(parameter) || shouldIncludeParameter(parameter))) return sortedParameters.every((item) => !!item.name);
}
return false;
}
// `ALL_ARGUMENTS.DATA_TYPE` carries no length, and PL/SQL rejects unconstrained string variables (PLS-00215).
// A CHAR formal parameter is blank-padded to the length of the variable passed in, so CHAR and NCHAR use the
// SQL column limits instead of the PL/SQL maximum. NVARCHAR2 is sized for the 3-byte UTF8 national character set.
function oracleVariableType(dataType: string): string {
const type = dataType.trim().toUpperCase();
if (type.includes("(")) return dataType.trim();
switch (type) {
case "VARCHAR2":
case "VARCHAR":
case "RAW":
return `${type}(32767)`;
case "NVARCHAR2":
return "NVARCHAR2(10922)";
case "CHAR":
return "CHAR(2000)";
case "NCHAR":
return "NCHAR(1000)";
case "REF CURSOR":
return "SYS_REFCURSOR";
case "PL/SQL BOOLEAN":
return "BOOLEAN";
default:
return dataType.trim();
}
}
// `DBMS_OUTPUT.PUT_LINE` raises ORU-10028 above 32767 bytes, which would fail the whole block after the
// procedure has run, so string values are truncated and LOB, RAW, BOOLEAN, and cursor values are not printed.
function oraclePrintableValue(dataType: string, variableName: string): string | null {
const type = dataType.trim().toUpperCase().replace(/\(.*$/, "").trim();
if (["VARCHAR2", "VARCHAR", "NVARCHAR2", "CHAR", "NCHAR"].includes(type)) return `SUBSTR(${variableName}, 1, 4000)`;
if (/^(NUMBER|FLOAT|INTEGER|BINARY_INTEGER|PLS_INTEGER|BINARY_FLOAT|BINARY_DOUBLE|DATE|ROWID|UROWID)$/.test(type)) return variableName;
if (/^(TIMESTAMP|INTERVAL)\b/.test(type)) return variableName;
return null;
}
function returnsRoutineOutput(parameter: Pick<RoutineParameterValue, "mode">): boolean {
return parameter.mode === "OUT" || parameter.mode === "INOUT";
}