Files
dbx/apps/desktop/src/lib/table/routineParameters.ts
T

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, "''")}'`;
}