fix(mysql): bind procedure output parameters

Closes #5330
This commit is contained in:
t8y2
2026-08-04 18:39:49 +08:00
parent 018c94cba1
commit dff33662fc
2 changed files with 95 additions and 0 deletions
@@ -7,6 +7,70 @@ function queryResult(columns: string[], rows: unknown[][]): QueryResult {
return { columns, rows, affected_rows: 0, execution_time_ms: 0 };
}
describe("MySQL routine execution SQL", () => {
it("binds OUT parameters without requiring user input", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "mysql",
schema: "app",
routineName: "double_value",
parameters: [
{ name: "p_input", dataType: "int", mode: "IN", ordinal: 1, value: "5" },
{ name: "p_output", dataType: "int", mode: "OUT", ordinal: 2, value: "ignored" },
],
});
expect(sql).toBe(["SET @dbx_output_2 = NULL;", "CALL `double_value`(5, @dbx_output_2);", "SELECT @dbx_output_2 AS `p_output`;"].join("\n"));
});
it("initializes INOUT variables and returns all output values", () => {
const sql = buildProcedureExecutionSqlFromValues({
databaseType: "mysql",
schema: "app",
routineName: "adjust_values",
parameters: [
{ name: "p_delta", dataType: "int", mode: "IN", ordinal: 1, value: "5" },
{ name: "p_total", dataType: "int", mode: "INOUT", ordinal: 2, value: "10" },
{ name: "p_status", dataType: "varchar(16)", mode: "OUT", ordinal: 3, value: "" },
],
});
expect(sql).toBe(["SET @dbx_output_2 = 10;", "SET @dbx_output_3 = NULL;", "CALL `adjust_values`(5, @dbx_output_2, @dbx_output_3);", "SELECT @dbx_output_2 AS `p_total`, @dbx_output_3 AS `p_status`;"].join("\n"));
});
it("keeps input-only procedure calls unchanged", () => {
expect(
buildProcedureExecutionSqlFromValues({
databaseType: "mysql",
schema: "app",
routineName: "save_value",
parameters: [{ name: "p_value", dataType: "varchar(32)", mode: "IN", ordinal: 1, value: "O'Reilly" }],
}),
).toBe("CALL `save_value`('O''Reilly');");
});
it("uses MySQL parameter modes to generate output bindings", () => {
const parameters = routineParametersFromResult(
queryResult(
["name", "data_type", "mode", "ordinal", "has_default"],
[
["p_input", "int", "IN", 1, false],
["p_output", "int", "OUT", 2, false],
],
),
"mysql",
);
expect(parameters.map((parameter) => parameter.mode)).toEqual(["IN", "OUT"]);
expect(
buildProcedureExecutionSqlFromValues({
databaseType: "mysql",
routineName: "double_value",
parameters: parameters.map((parameter) => ({ ...parameter, value: parameter.mode === "IN" ? "5" : "" })),
}),
).toContain("CALL `double_value`(5, @dbx_output_2);");
});
});
describe("SQL Server routine execution SQL", () => {
it("declares and selects OUT parameters", () => {
const sql = buildProcedureExecutionSqlFromValues({
@@ -44,6 +44,9 @@ export function buildProcedureExecutionSqlFromValues(options: BuildRoutineExecut
if (options.databaseType === "sqlserver") {
return buildSqlServerProcedureExecutionSql(routine, sortedParameters);
}
if (options.databaseType === "mysql") {
return buildMySqlProcedureExecutionSql(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") {
@@ -77,6 +80,34 @@ function sqlServerParameterName(name: string): string {
return name.startsWith("@") ? name : `@${name}`;
}
function buildMySqlProcedureExecutionSql(routine: string, sortedParameters: RoutineParameterValue[]): string {
const outputBindings = new Map<RoutineParameterValue, { variableName: string; alias: string }>();
const initializations: string[] = [];
sortedParameters.forEach((parameter, index) => {
if (!returnsRoutineOutput(parameter)) return;
const variableName = `@dbx_output_${index + 1}`;
const initialValue = parameter.mode === "INOUT" ? routineParameterSqlValue("mysql", parameter) : "NULL";
initializations.push(`SET ${variableName} = ${initialValue};`);
outputBindings.set(parameter, {
variableName,
alias: quoteTableIdentifier("mysql", parameter.name.replace(/^@/, "") || `output_${index + 1}`),
});
});
const args = sortedParameters.flatMap((parameter) => {
const outputBinding = outputBindings.get(parameter);
if (outputBinding) return [outputBinding.variableName];
if (!shouldIncludeParameter(parameter)) return [];
return [routineParameterSqlValue("mysql", parameter)];
});
const statements = [...initializations, `CALL ${routine}(${args.join(", ")});`];
if (outputBindings.size > 0) {
statements.push(`SELECT ${[...outputBindings.values()].map(({ variableName, alias }) => `${variableName} AS ${alias}`).join(", ")};`);
}
return statements.join("\n");
}
function buildSqlServerProcedureExecutionSql(routine: string, sortedParameters: RoutineParameterValue[]): string {
const outputBindings = new Map<RoutineParameterValue, { variableName: string; alias: string }>();
const declarations: string[] = [];