273 lines
9.8 KiB
TypeScript
273 lines
9.8 KiB
TypeScript
import * as api from "@/lib/backend/api";
|
|
import type { DatabaseType, QueryResult } from "@/types/database";
|
|
import type { RoutineParameter, RoutineParameterMode } from "@/lib/table/routineExecutionSql";
|
|
|
|
export interface LoadRoutineParametersOptions {
|
|
connectionId: string;
|
|
database: string;
|
|
databaseType?: DatabaseType;
|
|
schema?: string;
|
|
routineName: string;
|
|
}
|
|
|
|
export async function loadRoutineParameters(options: LoadRoutineParametersOptions): Promise<RoutineParameter[]> {
|
|
const sql = routineParametersQuery(options);
|
|
if (!sql) return [];
|
|
const result = await api.executeQuery(options.connectionId, options.database, sql, options.schema, undefined, {
|
|
maxRows: 200,
|
|
pageSize: 200,
|
|
});
|
|
return routineParametersFromResult(result, options.databaseType);
|
|
}
|
|
|
|
export function supportsRoutineParameterMetadata(databaseType?: DatabaseType): boolean {
|
|
return databaseType === "postgres" || databaseType === "mysql" || databaseType === "doris" || databaseType === "starrocks" || databaseType === "sqlserver" || databaseType === "oracle" || databaseType === "dameng" || databaseType === "oceanbase-oracle" || databaseType === "databend";
|
|
}
|
|
|
|
export function routineParametersQuery(options: Pick<LoadRoutineParametersOptions, "database" | "databaseType" | "schema" | "routineName">): string | null {
|
|
if (!supportsRoutineParameterMetadata(options.databaseType)) return null;
|
|
const effectiveSchema = options.schema || (options.databaseType === "postgres" ? "public" : "") || (options.databaseType === "mysql" || options.databaseType === "doris" || options.databaseType === "starrocks" ? options.database : "");
|
|
const schema = quoteSqlLiteral(effectiveSchema);
|
|
const name = quoteSqlLiteral(options.routineName);
|
|
if (options.databaseType === "postgres") {
|
|
return `
|
|
SELECT
|
|
NULLIF(arg.name, '') AS name,
|
|
arg.data_type,
|
|
CASE arg.mode
|
|
WHEN 'i' THEN 'IN'
|
|
WHEN 'o' THEN 'OUT'
|
|
WHEN 'b' THEN 'INOUT'
|
|
WHEN 'v' THEN 'IN'
|
|
WHEN 't' THEN 'OUT'
|
|
ELSE 'IN'
|
|
END AS mode,
|
|
arg.ordinal,
|
|
CASE
|
|
WHEN COALESCE(arg.mode, 'i') IN ('i', 'b', 'v') AND p.pronargdefaults > 0 AND arg.input_ordinal > p.pronargs - p.pronargdefaults THEN TRUE
|
|
ELSE FALSE
|
|
END AS has_default
|
|
FROM pg_proc p
|
|
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
CROSS JOIN LATERAL (
|
|
SELECT
|
|
gs.ordinal AS ordinal,
|
|
p.proargnames[gs.ordinal] AS name,
|
|
CASE
|
|
WHEN p.proallargtypes IS NULL THEN p.proargtypes[gs.ordinal - 1]
|
|
ELSE p.proallargtypes[gs.ordinal]
|
|
END::regtype::text AS data_type,
|
|
COALESCE(p.proargmodes[gs.ordinal], 'i') AS mode,
|
|
COUNT(*) FILTER (WHERE COALESCE(p.proargmodes[gs.ordinal], 'i') IN ('i', 'b', 'v')) OVER (ORDER BY gs.ordinal) AS input_ordinal
|
|
FROM generate_series(1, COALESCE(array_length(p.proallargtypes, 1), p.pronargs)) AS gs(ordinal)
|
|
) arg
|
|
WHERE p.prokind = 'p'
|
|
AND n.nspname = ${schema}
|
|
AND p.proname = ${name}
|
|
ORDER BY arg.ordinal;`.trim();
|
|
}
|
|
if (options.databaseType === "mysql" || options.databaseType === "doris" || options.databaseType === "starrocks") {
|
|
return `
|
|
SELECT
|
|
PARAMETER_NAME AS name,
|
|
DTD_IDENTIFIER AS data_type,
|
|
COALESCE(PARAMETER_MODE, 'IN') AS mode,
|
|
ORDINAL_POSITION AS ordinal,
|
|
FALSE AS has_default
|
|
FROM information_schema.PARAMETERS
|
|
WHERE SPECIFIC_SCHEMA = ${schema}
|
|
AND SPECIFIC_NAME = ${name}
|
|
AND ORDINAL_POSITION > 0
|
|
ORDER BY ORDINAL_POSITION;`.trim();
|
|
}
|
|
if (options.databaseType === "databend") {
|
|
return `
|
|
SELECT arguments
|
|
FROM system.procedures
|
|
WHERE name = ${name}
|
|
ORDER BY procedure_id
|
|
LIMIT 1;`.trim();
|
|
}
|
|
if (options.databaseType === "sqlserver") {
|
|
return `
|
|
SELECT
|
|
p.name AS name,
|
|
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.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}
|
|
ORDER BY p.parameter_id;`.trim();
|
|
}
|
|
if (options.databaseType === "oracle" || options.databaseType === "dameng" || options.databaseType === "oceanbase-oracle") {
|
|
return `
|
|
SELECT
|
|
ARGUMENT_NAME AS name,
|
|
DATA_TYPE AS data_type,
|
|
IN_OUT AS mode,
|
|
POSITION AS ordinal,
|
|
DEFAULTED AS has_default
|
|
FROM ALL_ARGUMENTS
|
|
WHERE OWNER = UPPER(${schema})
|
|
AND OBJECT_NAME = UPPER(${name})
|
|
AND POSITION > 0
|
|
ORDER BY SEQUENCE;`.trim();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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) => {
|
|
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] || "");
|
|
const inputTypes = databendInputTypesFromArguments(signature);
|
|
return inputTypes.map((dataType, index) => ({
|
|
name: `arg${index + 1}`,
|
|
dataType,
|
|
mode: "IN",
|
|
ordinal: index + 1,
|
|
hasDefault: false,
|
|
}));
|
|
}
|
|
|
|
function databendInputTypesFromArguments(signature: string): string[] {
|
|
const openIndex = signature.indexOf("(");
|
|
if (openIndex < 0) return [];
|
|
let depth = 0;
|
|
for (let index = openIndex; index < signature.length; index += 1) {
|
|
const char = signature[index];
|
|
if (char === "(") depth += 1;
|
|
if (char === ")") {
|
|
depth -= 1;
|
|
if (depth === 0) {
|
|
return splitTopLevelComma(signature.slice(openIndex + 1, index)).filter(Boolean);
|
|
}
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function splitTopLevelComma(value: string): string[] {
|
|
const parts: string[] = [];
|
|
let current = "";
|
|
let depth = 0;
|
|
for (const char of value) {
|
|
if (char === "(") depth += 1;
|
|
if (char === ")") depth = Math.max(0, depth - 1);
|
|
if (char === "," && depth === 0) {
|
|
parts.push(current.trim());
|
|
current = "";
|
|
continue;
|
|
}
|
|
current += char;
|
|
}
|
|
if (current.trim()) parts.push(current.trim());
|
|
return parts;
|
|
}
|
|
|
|
function normalizeParameterMode(value: unknown): RoutineParameterMode {
|
|
const mode = String(value || "IN")
|
|
.toUpperCase()
|
|
.replace(/\s+/g, "");
|
|
if (mode === "IN") return "IN";
|
|
if (mode === "OUT") return "OUT";
|
|
if (mode === "INOUT" || mode === "IN/OUT") return "INOUT";
|
|
if (mode === "RETURN") return "RETURN";
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
function normalizeBoolean(value: unknown): boolean {
|
|
if (typeof value === "boolean") return value;
|
|
if (typeof value === "number") return value !== 0;
|
|
const text = String(value || "").toLowerCase();
|
|
return text === "true" || text === "yes" || text === "y" || text === "1";
|
|
}
|
|
|
|
function quoteSqlLiteral(value: string): string {
|
|
return `'${value.replace(/'/g, "''")}'`;
|
|
}
|