mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
fix(oracle): bind out parameters when executing procedures
This commit is contained in:
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user