fix(sqlserver): generate procedure output parameters
This commit is contained in:
parent
348cbc0870
commit
a662d12f57
|
|
@ -0,0 +1,91 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildProcedureExecutionSqlFromValues } from "@/lib/table/routineExecutionSql";
|
||||
import { routineParametersFromResult, routineParametersQuery } from "@/lib/table/routineParameters";
|
||||
import type { QueryResult } from "@/types/database";
|
||||
|
||||
function queryResult(columns: string[], rows: unknown[][]): QueryResult {
|
||||
return { columns, rows, affected_rows: 0, execution_time_ms: 0 };
|
||||
}
|
||||
|
||||
describe("SQL Server routine execution SQL", () => {
|
||||
it("declares and selects OUT parameters", () => {
|
||||
const sql = buildProcedureExecutionSqlFromValues({
|
||||
databaseType: "sqlserver",
|
||||
schema: "dbo",
|
||||
routineName: "Sys_CreatePrimaryKeyValue",
|
||||
parameters: [
|
||||
{ name: "@tableName", dataType: "varchar(64)", mode: "IN", ordinal: 1, value: "users" },
|
||||
{ name: "@returnValue", dataType: "varchar(128)", mode: "OUT", ordinal: 2, value: "" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(sql).toBe(["DECLARE @dbx_output_2 varchar(128);", "EXEC [dbo].[Sys_CreatePrimaryKeyValue] @tableName = 'users', @returnValue = @dbx_output_2 OUTPUT;", "SELECT @dbx_output_2 AS [returnValue];"].join("\n"));
|
||||
});
|
||||
|
||||
it("initializes INOUT parameters and keeps numeric values unquoted", () => {
|
||||
const sql = buildProcedureExecutionSqlFromValues({
|
||||
databaseType: "sqlserver",
|
||||
schema: "dbo",
|
||||
routineName: "adjust_amount",
|
||||
parameters: [{ name: "@amount", dataType: "decimal(18,4)", mode: "INOUT", ordinal: 1, value: "12.5000" }],
|
||||
});
|
||||
|
||||
expect(sql).toBe(["DECLARE @dbx_output_1 decimal(18,4) = 12.5000;", "EXEC [dbo].[adjust_amount] @amount = @dbx_output_1 OUTPUT;", "SELECT @dbx_output_1 AS [amount];"].join("\n"));
|
||||
});
|
||||
|
||||
it("preserves IN parameters and omission of requested defaults", () => {
|
||||
const sql = buildProcedureExecutionSqlFromValues({
|
||||
databaseType: "sqlserver",
|
||||
schema: "dbo",
|
||||
routineName: "refresh_cache",
|
||||
parameters: [
|
||||
{ name: "@scope", dataType: "varchar(32)", mode: "IN", ordinal: 1, value: "all" },
|
||||
{ name: "@timeout", dataType: "int", mode: "IN", ordinal: 2, value: "30", hasDefault: true, useDefault: true },
|
||||
],
|
||||
});
|
||||
|
||||
expect(sql).toBe("EXEC [dbo].[refresh_cache] @scope = 'all';");
|
||||
});
|
||||
|
||||
it("keeps no-parameter procedures valid and ignores RETURN metadata", () => {
|
||||
expect(buildProcedureExecutionSqlFromValues({ databaseType: "sqlserver", schema: "dbo", routineName: "ping", parameters: [] })).toBe("EXEC [dbo].[ping];");
|
||||
expect(
|
||||
buildProcedureExecutionSqlFromValues({
|
||||
databaseType: "sqlserver",
|
||||
schema: "dbo",
|
||||
routineName: "ping",
|
||||
parameters: [{ name: "return_status", dataType: "int", mode: "RETURN", ordinal: 0, value: "" }],
|
||||
}),
|
||||
).toBe("EXEC [dbo].[ping];");
|
||||
});
|
||||
|
||||
it("preserves SQL Server declaration lengths, MAX, and decimal precision", () => {
|
||||
const parameters = routineParametersFromResult(
|
||||
queryResult(
|
||||
["name", "data_type", "mode", "ordinal", "has_default", "max_length", "precision", "scale", "type_schema", "is_user_defined"],
|
||||
[
|
||||
["@short", "varchar", "OUT", 1, false, 64, 0, 0, "sys", false],
|
||||
["@long", "varchar", "OUT", 2, false, -1, 0, 0, "sys", false],
|
||||
["@amount", "decimal", "OUT", 3, false, 17, 18, 4, "sys", false],
|
||||
],
|
||||
),
|
||||
"sqlserver",
|
||||
);
|
||||
|
||||
expect(parameters.map((parameter) => parameter.dataType)).toEqual(["varchar(64)", "varchar(max)", "decimal(18,4)"]);
|
||||
expect(
|
||||
buildProcedureExecutionSqlFromValues({
|
||||
databaseType: "sqlserver",
|
||||
schema: "dbo",
|
||||
routineName: "collect_outputs",
|
||||
parameters: parameters.map((parameter) => ({ ...parameter, value: "" })),
|
||||
}),
|
||||
).toContain(["DECLARE @dbx_output_1 varchar(64);", "DECLARE @dbx_output_2 varchar(max);", "DECLARE @dbx_output_3 decimal(18,4);"].join("\n"));
|
||||
|
||||
const metadataSql = routineParametersQuery({ database: "app", databaseType: "sqlserver", schema: "dbo", routineName: "save" });
|
||||
expect(metadataSql).toContain("JOIN sys.types t ON t.user_type_id = p.user_type_id");
|
||||
expect(metadataSql).toContain("p.max_length AS max_length");
|
||||
expect(metadataSql).toContain("p.precision AS precision");
|
||||
expect(metadataSql).toContain("p.scale AS scale");
|
||||
});
|
||||
});
|
||||
|
|
@ -41,12 +41,11 @@ export function buildProcedureExecutionSql(options: BuildRoutineExecutionSqlOpti
|
|||
export function buildProcedureExecutionSqlFromValues(options: BuildRoutineExecutionSqlOptions & { parameters: RoutineParameterValue[] }): string {
|
||||
const routine = qualifiedRoutineName(options);
|
||||
const sortedParameters = [...options.parameters].sort((a, b) => a.ordinal - b.ordinal);
|
||||
if (options.databaseType === "sqlserver") {
|
||||
return buildSqlServerProcedureExecutionSql(routine, sortedParameters);
|
||||
}
|
||||
const values = sortedParameters.filter((parameter) => shouldIncludeParameter(parameter));
|
||||
const useNamedArguments = shouldUseNamedArguments(options.databaseType, sortedParameters);
|
||||
if (options.databaseType === "sqlserver") {
|
||||
const args = values.map((parameter) => `${sqlServerParameterName(parameter.name)} = ${routineParameterSqlValue(options.databaseType, parameter)}`).join(", ");
|
||||
return args ? `EXEC ${routine} ${args};` : `EXEC ${routine};`;
|
||||
}
|
||||
if (options.databaseType === "oracle" || options.databaseType === "dameng" || options.databaseType === "oceanbase-oracle") {
|
||||
return `BEGIN\n ${routine}(${values.map((parameter) => routineArgumentSql(options.databaseType, parameter, useNamedArguments)).join(", ")});\nEND;`;
|
||||
}
|
||||
|
|
@ -78,6 +77,45 @@ function sqlServerParameterName(name: string): string {
|
|||
return name.startsWith("@") ? name : `@${name}`;
|
||||
}
|
||||
|
||||
function buildSqlServerProcedureExecutionSql(routine: string, sortedParameters: RoutineParameterValue[]): string {
|
||||
const outputBindings = new Map<RoutineParameterValue, { variableName: string; alias: string }>();
|
||||
const declarations: string[] = [];
|
||||
|
||||
sortedParameters.forEach((parameter, index) => {
|
||||
if (!returnsRoutineOutput(parameter)) return;
|
||||
if (parameter.mode === "INOUT" && parameter.useDefault && parameter.hasDefault) return;
|
||||
const declarationType = parameter.dataType.trim();
|
||||
if (!declarationType) return;
|
||||
|
||||
const variableName = `@dbx_output_${index + 1}`;
|
||||
const initialValue = parameter.mode === "INOUT" ? ` = ${routineParameterSqlValue("sqlserver", parameter)}` : "";
|
||||
declarations.push(`DECLARE ${variableName} ${declarationType}${initialValue};`);
|
||||
outputBindings.set(parameter, {
|
||||
variableName,
|
||||
alias: quoteTableIdentifier("sqlserver", parameter.name.replace(/^@/, "") || `output_${index + 1}`),
|
||||
});
|
||||
});
|
||||
|
||||
const args = sortedParameters.flatMap((parameter) => {
|
||||
const outputBinding = outputBindings.get(parameter);
|
||||
if (outputBinding) {
|
||||
return [`${sqlServerParameterName(parameter.name)} = ${outputBinding.variableName} OUTPUT`];
|
||||
}
|
||||
if (returnsRoutineOutput(parameter) || !shouldIncludeParameter(parameter)) return [];
|
||||
return [`${sqlServerParameterName(parameter.name)} = ${routineParameterSqlValue("sqlserver", parameter)}`];
|
||||
});
|
||||
const statements = [...declarations, args.length > 0 ? `EXEC ${routine} ${args.join(", ")};` : `EXEC ${routine};`];
|
||||
|
||||
if (outputBindings.size > 0) {
|
||||
statements.push(`SELECT ${[...outputBindings.values()].map(({ variableName, alias }) => `${variableName} AS ${alias}`).join(", ")};`);
|
||||
}
|
||||
return statements.join("\n");
|
||||
}
|
||||
|
||||
function returnsRoutineOutput(parameter: Pick<RoutineParameterValue, "mode">): boolean {
|
||||
return parameter.mode === "OUT" || parameter.mode === "INOUT";
|
||||
}
|
||||
|
||||
function routineArgumentSql(databaseType: DatabaseType | undefined, parameter: RoutineParameterValue, useNamedArguments: boolean): string {
|
||||
const value = routineParameterSqlValue(databaseType, parameter);
|
||||
if (!useNamedArguments) return value;
|
||||
|
|
|
|||
|
|
@ -92,13 +92,19 @@ LIMIT 1;`.trim();
|
|||
return `
|
||||
SELECT
|
||||
p.name AS name,
|
||||
TYPE_NAME(p.user_type_id) AS data_type,
|
||||
t.name AS data_type,
|
||||
CASE WHEN p.is_output = 1 THEN 'OUT' ELSE 'IN' END AS mode,
|
||||
p.parameter_id AS ordinal,
|
||||
p.has_default_value AS has_default
|
||||
p.has_default_value AS has_default,
|
||||
p.max_length AS max_length,
|
||||
p.precision AS precision,
|
||||
p.scale AS scale,
|
||||
SCHEMA_NAME(t.schema_id) AS type_schema,
|
||||
t.is_user_defined AS is_user_defined
|
||||
FROM sys.parameters p
|
||||
JOIN sys.objects o ON o.object_id = p.object_id
|
||||
JOIN sys.schemas s ON s.schema_id = o.schema_id
|
||||
JOIN sys.types t ON t.user_type_id = p.user_type_id
|
||||
WHERE o.type IN ('P', 'PC')
|
||||
AND s.name = ${schema}
|
||||
AND o.name = ${name}
|
||||
|
|
@ -123,17 +129,78 @@ ORDER BY SEQUENCE;`.trim();
|
|||
|
||||
export function routineParametersFromResult(result: QueryResult, databaseType?: DatabaseType): RoutineParameter[] {
|
||||
if (databaseType === "databend") return databendRoutineParametersFromResult(result);
|
||||
const sqlServerMetadata =
|
||||
databaseType === "sqlserver"
|
||||
? {
|
||||
maxLength: result.columns.findIndex((column) => column.toLowerCase() === "max_length"),
|
||||
precision: result.columns.findIndex((column) => column.toLowerCase() === "precision"),
|
||||
scale: result.columns.findIndex((column) => column.toLowerCase() === "scale"),
|
||||
typeSchema: result.columns.findIndex((column) => column.toLowerCase() === "type_schema"),
|
||||
isUserDefined: result.columns.findIndex((column) => column.toLowerCase() === "is_user_defined"),
|
||||
}
|
||||
: null;
|
||||
return result.rows
|
||||
.map((row, index) => ({
|
||||
name: String(row[0] || `arg${index + 1}`),
|
||||
dataType: String(row[1] || ""),
|
||||
mode: normalizeParameterMode(row[2]),
|
||||
ordinal: Number(row[3] || index + 1),
|
||||
hasDefault: normalizeBoolean(row[4]),
|
||||
}))
|
||||
.map((row, index) => {
|
||||
const dataType = String(row[1] || "");
|
||||
return {
|
||||
name: String(row[0] || `arg${index + 1}`),
|
||||
dataType: sqlServerMetadata ? sqlServerParameterDeclarationType(dataType, row, sqlServerMetadata) : dataType,
|
||||
mode: normalizeParameterMode(row[2]),
|
||||
ordinal: Number(row[3] || index + 1),
|
||||
hasDefault: normalizeBoolean(row[4]),
|
||||
};
|
||||
})
|
||||
.filter((parameter) => parameter.mode !== "RETURN");
|
||||
}
|
||||
|
||||
interface SqlServerParameterMetadataIndexes {
|
||||
maxLength: number;
|
||||
precision: number;
|
||||
scale: number;
|
||||
typeSchema: number;
|
||||
isUserDefined: number;
|
||||
}
|
||||
|
||||
function sqlServerParameterDeclarationType(baseType: string, row: unknown[], indexes: SqlServerParameterMetadataIndexes): string {
|
||||
const typeName = baseType.trim();
|
||||
if (!typeName) return "";
|
||||
if (normalizeBoolean(valueAt(row, indexes.isUserDefined))) {
|
||||
const schema = String(valueAt(row, indexes.typeSchema) || "").trim();
|
||||
const qualifiedType = quoteSqlServerIdentifier(typeName);
|
||||
return schema ? `${quoteSqlServerIdentifier(schema)}.${qualifiedType}` : qualifiedType;
|
||||
}
|
||||
|
||||
const normalizedType = typeName.toLowerCase();
|
||||
const maxLength = Number(valueAt(row, indexes.maxLength));
|
||||
if (["varchar", "char", "varbinary", "binary"].includes(normalizedType) && Number.isFinite(maxLength)) {
|
||||
return `${typeName}(${maxLength === -1 ? "max" : Math.max(1, maxLength)})`;
|
||||
}
|
||||
if (["nvarchar", "nchar"].includes(normalizedType) && Number.isFinite(maxLength)) {
|
||||
return `${typeName}(${maxLength === -1 ? "max" : Math.max(1, Math.floor(maxLength / 2))})`;
|
||||
}
|
||||
|
||||
const precision = Number(valueAt(row, indexes.precision));
|
||||
const scale = Number(valueAt(row, indexes.scale));
|
||||
if (["decimal", "numeric"].includes(normalizedType) && Number.isFinite(precision) && Number.isFinite(scale)) {
|
||||
return `${typeName}(${precision},${scale})`;
|
||||
}
|
||||
if (["datetime2", "datetimeoffset", "time"].includes(normalizedType) && Number.isFinite(scale)) {
|
||||
return `${typeName}(${scale})`;
|
||||
}
|
||||
if (normalizedType === "float" && Number.isFinite(precision)) {
|
||||
return `${typeName}(${precision})`;
|
||||
}
|
||||
return typeName;
|
||||
}
|
||||
|
||||
function valueAt(row: unknown[], index: number): unknown {
|
||||
return index >= 0 ? row[index] : undefined;
|
||||
}
|
||||
|
||||
function quoteSqlServerIdentifier(value: string): string {
|
||||
return `[${value.replace(/]/g, "]]")}]`;
|
||||
}
|
||||
|
||||
function databendRoutineParametersFromResult(result: QueryResult): RoutineParameter[] {
|
||||
const argumentsIndex = result.columns.findIndex((column) => column.toLowerCase() === "arguments");
|
||||
const signature = String(result.rows[0]?.[argumentsIndex >= 0 ? argumentsIndex : 0] || "");
|
||||
|
|
|
|||
Loading…
Reference in New Issue