288 lines
13 KiB
TypeScript
Vendored
288 lines
13 KiB
TypeScript
Vendored
import { describe, expect, it } from "vitest";
|
|
import { buildProcedureExecutionSqlFromValues } from "@/lib/table/routineExecutionSql";
|
|
import { routineParametersFromResult, routineParametersQuery, supportsRoutineParameterMetadata, xuguRoutineMetadataFromDefinition } 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("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({
|
|
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");
|
|
});
|
|
|
|
it("quotes Oracle routine metadata aliases for legacy Oracle syntax", () => {
|
|
const sql = routineParametersQuery({ database: "XE", databaseType: "oracle", schema: "DBX_TEST", routineName: "demo" });
|
|
|
|
expect(sql).toContain('ARGUMENT_NAME AS "name"');
|
|
expect(sql).toContain('DEFAULTED AS "has_default"');
|
|
});
|
|
});
|
|
|
|
describe("XuguDB routine parameter metadata", () => {
|
|
it("parses modes, nested types, defaults, comments, and quoted identifiers", () => {
|
|
const metadata = xuguRoutineMetadataFromDefinition(`
|
|
CREATE OR REPLACE PROCEDURE "AppSchema"."MixedCaseProcedure" (
|
|
"p_required" IN INTEGER,
|
|
p_text VARCHAR(50) DEFAULT 'a,b -- literal',
|
|
p_amount IN /* mode separator */ OUT NUMERIC(12, 3) := -1.250,
|
|
p_result OUT VARCHAR(100),
|
|
p_comment IN VARCHAR(40) DEFAULT '/* literal */'
|
|
) AS
|
|
BEGIN
|
|
NULL;
|
|
END;`);
|
|
|
|
expect(metadata.kind).toBe("PROCEDURE");
|
|
expect(metadata.returnType).toBeUndefined();
|
|
expect(metadata.parameters).toEqual([
|
|
{ name: "p_required", dataType: "INTEGER", mode: "IN", ordinal: 1, hasDefault: false, defaultValue: undefined },
|
|
{ name: "p_text", dataType: "VARCHAR(50)", mode: "IN", ordinal: 2, hasDefault: true, defaultValue: "'a,b -- literal'" },
|
|
{ name: "p_amount", dataType: "NUMERIC(12, 3)", mode: "INOUT", ordinal: 3, hasDefault: true, defaultValue: "-1.250" },
|
|
{ name: "p_result", dataType: "VARCHAR(100)", mode: "OUT", ordinal: 4, hasDefault: false, defaultValue: undefined },
|
|
{ name: "p_comment", dataType: "VARCHAR(40)", mode: "IN", ordinal: 5, hasDefault: true, defaultValue: "'/* literal */'" },
|
|
]);
|
|
});
|
|
|
|
it("parses function parameters and its return type", () => {
|
|
const metadata = xuguRoutineMetadataFromDefinition(`
|
|
CREATE OR REPLACE FUNCTION calculate_total(
|
|
p_amount IN NUMERIC(10,2),
|
|
p_rate NUMERIC(5,2) DEFAULT 0.10
|
|
) RETURN NUMERIC(12,3)
|
|
AS
|
|
BEGIN
|
|
RETURN p_amount * p_rate;
|
|
END;`);
|
|
|
|
expect(metadata).toEqual({
|
|
kind: "FUNCTION",
|
|
returnType: "NUMERIC(12,3)",
|
|
parameters: [
|
|
{ name: "p_amount", dataType: "NUMERIC(10,2)", mode: "IN", ordinal: 1, hasDefault: false, defaultValue: undefined },
|
|
{ name: "p_rate", dataType: "NUMERIC(5,2)", mode: "IN", ordinal: 2, hasDefault: true, defaultValue: "0.10" },
|
|
],
|
|
});
|
|
});
|
|
|
|
it("handles routines without parentheses and fails closed for malformed headers", () => {
|
|
expect(xuguRoutineMetadataFromDefinition("CREATE FUNCTION ping RETURN INTEGER AS BEGIN RETURN 1; END;")).toEqual({ kind: "FUNCTION", parameters: [], returnType: "INTEGER" });
|
|
expect(xuguRoutineMetadataFromDefinition("CREATE PROCEDURE broken(p_value IN INTEGER AS BEGIN NULL; END;")).toEqual({ kind: "PROCEDURE", parameters: [] });
|
|
expect(xuguRoutineMetadataFromDefinition("CREATE FUNCTION broken(p_value INTEGER) AS BEGIN RETURN p_value; END;")).toEqual({
|
|
kind: "FUNCTION",
|
|
parameters: [{ name: "p_value", dataType: "INTEGER", mode: "IN", ordinal: 1, hasDefault: false, defaultValue: undefined }],
|
|
returnType: undefined,
|
|
});
|
|
expect(xuguRoutineMetadataFromDefinition("CREATE PROCEDURE broken(p_value VARCHAR DEFAULT 'unterminated) AS BEGIN NULL; END;")).toEqual({ parameters: [] });
|
|
expect(xuguRoutineMetadataFromDefinition("CREATE PROCEDURE broken(/* unterminated")).toEqual({ parameters: [] });
|
|
expect(xuguRoutineMetadataFromDefinition("SELECT 'PROCEDURE fake(p INT)' FROM dual")).toEqual({ parameters: [] });
|
|
});
|
|
|
|
it("supports INOUT spelling, escaped names, and equals defaults", () => {
|
|
expect(xuguRoutineMetadataFromDefinition('CREATE PROCEDURE p("p""name" INOUT DECIMAL(9,2) = COALESCE(-1, 0)) AS BEGIN NULL; END;').parameters).toEqual([
|
|
{
|
|
name: 'p"name',
|
|
dataType: "DECIMAL(9,2)",
|
|
mode: "INOUT",
|
|
ordinal: 1,
|
|
hasDefault: true,
|
|
defaultValue: "COALESCE(-1, 0)",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("enables source-backed metadata without generating an ALL_ARGUMENTS query", () => {
|
|
expect(supportsRoutineParameterMetadata("xugu")).toBe(true);
|
|
expect(routineParametersQuery({ database: "sample", databaseType: "xugu", schema: "app", routineName: "save_value" })).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("XuguDB procedure execution SQL", () => {
|
|
it("supplies OUT arguments and preserves INOUT input values", () => {
|
|
const sql = buildProcedureExecutionSqlFromValues({
|
|
databaseType: "xugu",
|
|
schema: "AppSchema",
|
|
routineName: "adjust_amount",
|
|
parameters: [
|
|
{ name: "p_input", dataType: "INTEGER", mode: "IN", ordinal: 1, value: "5" },
|
|
{ name: "p_amount", dataType: "NUMERIC(12,3)", mode: "INOUT", ordinal: 2, value: "2.500" },
|
|
{ name: "p_status", dataType: "VARCHAR(20)", mode: "OUT", ordinal: 3, value: "ignored" },
|
|
],
|
|
});
|
|
|
|
expect(sql).toBe('CALL "AppSchema"."adjust_amount"(5, 2.500, NULL);');
|
|
});
|
|
|
|
it("uses named notation when a middle default is omitted", () => {
|
|
const sql = buildProcedureExecutionSqlFromValues({
|
|
databaseType: "xugu",
|
|
schema: "AppSchema",
|
|
routineName: "save_value",
|
|
parameters: [
|
|
{ name: "p_required", dataType: "INTEGER", mode: "IN", ordinal: 1, value: "1" },
|
|
{ name: "p_label", dataType: "VARCHAR(20)", mode: "IN", ordinal: 2, value: "unused", hasDefault: true, useDefault: true },
|
|
{ name: "p_amount", dataType: "NUMERIC(12,3)", mode: "INOUT", ordinal: 3, value: "2.500" },
|
|
{ name: "p_result", dataType: "VARCHAR(20)", mode: "OUT", ordinal: 4, value: "" },
|
|
],
|
|
});
|
|
|
|
expect(sql).toBe('CALL "AppSchema"."save_value"("p_required" => 1, "p_amount" => 2.500, "p_result" => NULL);');
|
|
});
|
|
|
|
it("omits trailing defaults positionally and escapes string values", () => {
|
|
const sql = buildProcedureExecutionSqlFromValues({
|
|
databaseType: "xugu",
|
|
schema: "AppSchema",
|
|
routineName: "save_label",
|
|
parameters: [
|
|
{ name: "p_label", dataType: "VARCHAR(30)", mode: "IN", ordinal: 1, value: "O'Reilly" },
|
|
{ name: "p_enabled", dataType: "BOOLEAN", mode: "IN", ordinal: 2, value: "true", hasDefault: true, useDefault: true },
|
|
],
|
|
});
|
|
|
|
expect(sql).toBe("CALL \"AppSchema\".\"save_label\"('O''Reilly');");
|
|
});
|
|
});
|