diff --git a/apps/desktop/src/lib/__tests__/table/routineExecutionSql.spec.ts b/apps/desktop/src/lib/__tests__/table/routineExecutionSql.spec.ts index e08bb1704e..cd25958029 100644 --- a/apps/desktop/src/lib/__tests__/table/routineExecutionSql.spec.ts +++ b/apps/desktop/src/lib/__tests__/table/routineExecutionSql.spec.ts @@ -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({ diff --git a/apps/desktop/src/lib/table/routineExecutionSql.ts b/apps/desktop/src/lib/table/routineExecutionSql.ts index 5b889990fe..c52961f062 100644 --- a/apps/desktop/src/lib/table/routineExecutionSql.ts +++ b/apps/desktop/src/lib/table/routineExecutionSql.ts @@ -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(); + 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(); const declarations: string[] = [];