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 { if (options.databaseType === "xugu") { const source = await api.getObjectSource(options.connectionId, options.database, options.schema || options.database, options.routineName, "PROCEDURE"); return xuguRoutineMetadataFromDefinition(source.source).parameters; } 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" || databaseType === "xugu" ); } export function routineParametersQuery(options: Pick): 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 interface XuguRoutineMetadata { kind?: "PROCEDURE" | "FUNCTION"; parameters: RoutineParameter[]; returnType?: string; } interface XuguRoutineToken { kind: "word" | "quoted-identifier" | "string" | "symbol"; text: string; start: number; end: number; } /** * XuguDB does not expose ALL_ARGUMENTS on every supported server version. * Its DBeaver extension therefore parses ALL_PROCEDURES.DEFINE as well. Keep * this parser deliberately limited to the declaration header: the PL/SQL body * is never interpreted and malformed definitions fail closed with no metadata. */ export function xuguRoutineMetadataFromDefinition(definition: string): XuguRoutineMetadata { const tokens = tokenizeXuguRoutineDefinition(definition); if (!tokens) return { parameters: [] }; const kindIndex = tokens.findIndex((token) => isWord(token, "PROCEDURE") || isWord(token, "FUNCTION")); if (kindIndex < 0) return { parameters: [] }; const kind = tokens[kindIndex].text.toUpperCase() as "PROCEDURE" | "FUNCTION"; const nameEndIndex = xuguRoutineNameEndIndex(tokens, kindIndex + 1); if (nameEndIndex < 0) return { kind, parameters: [] }; let headerIndex = nameEndIndex; let parameterCloseIndex = -1; let parameters: RoutineParameter[] = []; if (tokens[headerIndex]?.text === "(") { parameterCloseIndex = matchingTokenParenIndex(tokens, headerIndex); if (parameterCloseIndex < 0) return { kind, parameters: [] }; parameters = parseXuguRoutineParameters(definition, tokens, headerIndex + 1, parameterCloseIndex); headerIndex = parameterCloseIndex + 1; } const returnType = kind === "FUNCTION" ? xuguFunctionReturnType(definition, tokens, headerIndex) : undefined; return { kind, parameters, returnType }; } function tokenizeXuguRoutineDefinition(definition: string): XuguRoutineToken[] | null { const tokens: XuguRoutineToken[] = []; let index = 0; while (index < definition.length) { const char = definition[index]; if (/\s/.test(char)) { index += 1; continue; } if (char === "-" && definition[index + 1] === "-") { index += 2; while (index < definition.length && definition[index] !== "\n" && definition[index] !== "\r") index += 1; continue; } if (char === "/" && definition[index + 1] === "*") { const close = definition.indexOf("*/", index + 2); if (close < 0) return null; index = close + 2; continue; } if (char === "'" || char === '"') { const start = index; const quote = char; let closed = false; index += 1; while (index < definition.length) { if (definition[index] !== quote) { index += 1; continue; } if (definition[index + 1] === quote) { index += 2; continue; } index += 1; closed = true; break; } if (!closed) return null; tokens.push({ kind: quote === "'" ? "string" : "quoted-identifier", text: definition.slice(start, index), start, end: index }); continue; } if (/[A-Za-z_#$]/.test(char)) { const start = index; index += 1; while (index < definition.length && /[A-Za-z0-9_#$%]/.test(definition[index])) index += 1; tokens.push({ kind: "word", text: definition.slice(start, index), start, end: index }); continue; } const start = index; if (char === ":" && definition[index + 1] === "=") index += 2; else index += 1; tokens.push({ kind: "symbol", text: definition.slice(start, index), start, end: index }); } return tokens; } function xuguRoutineNameEndIndex(tokens: XuguRoutineToken[], startIndex: number): number { const first = tokens[startIndex]; if (!isIdentifierToken(first)) return -1; let index = startIndex + 1; while (tokens[index]?.text === "." && isIdentifierToken(tokens[index + 1])) index += 2; return index; } function isIdentifierToken(token?: XuguRoutineToken): boolean { return token?.kind === "word" || token?.kind === "quoted-identifier"; } function isWord(token: XuguRoutineToken | undefined, word: string): boolean { return token?.kind === "word" && token.text.toUpperCase() === word; } function matchingTokenParenIndex(tokens: XuguRoutineToken[], openIndex: number): number { let depth = 0; for (let index = openIndex; index < tokens.length; index += 1) { if (tokens[index].text === "(") depth += 1; if (tokens[index].text === ")") { depth -= 1; if (depth === 0) return index; } } return -1; } function parseXuguRoutineParameters(definition: string, tokens: XuguRoutineToken[], startIndex: number, endIndex: number): RoutineParameter[] { const ranges: Array<[number, number]> = []; let depth = 0; let rangeStart = startIndex; for (let index = startIndex; index < endIndex; index += 1) { if (tokens[index].text === "(") depth += 1; if (tokens[index].text === ")") depth = Math.max(0, depth - 1); if (tokens[index].text === "," && depth === 0) { ranges.push([rangeStart, index]); rangeStart = index + 1; } } ranges.push([rangeStart, endIndex]); return ranges.flatMap(([start, end], ordinalIndex) => { const parameter = parseXuguRoutineParameter(definition, tokens, start, end, ordinalIndex + 1); return parameter ? [parameter] : []; }); } function parseXuguRoutineParameter(definition: string, tokens: XuguRoutineToken[], startIndex: number, endIndex: number, ordinal: number): RoutineParameter | null { if (startIndex >= endIndex || !isIdentifierToken(tokens[startIndex])) return null; const name = unquoteXuguIdentifier(tokens[startIndex].text); let typeStartIndex = startIndex + 1; let mode: RoutineParameterMode = "IN"; if (isWord(tokens[typeStartIndex], "INOUT")) { mode = "INOUT"; typeStartIndex += 1; } else if (isWord(tokens[typeStartIndex], "IN")) { if (isWord(tokens[typeStartIndex + 1], "OUT")) { mode = "INOUT"; typeStartIndex += 2; } else { mode = "IN"; typeStartIndex += 1; } } else if (isWord(tokens[typeStartIndex], "OUT")) { mode = "OUT"; typeStartIndex += 1; } let depth = 0; let defaultIndex = -1; for (let index = typeStartIndex; index < endIndex; index += 1) { if (tokens[index].text === "(") depth += 1; if (tokens[index].text === ")") depth = Math.max(0, depth - 1); if (depth === 0 && (isWord(tokens[index], "DEFAULT") || tokens[index].text === ":=" || tokens[index].text === "=")) { defaultIndex = index; break; } } const typeEndIndex = defaultIndex >= 0 ? defaultIndex : endIndex; if (typeStartIndex >= typeEndIndex) return null; const dataType = xuguTokenRangeText(definition, tokens, typeStartIndex, typeEndIndex).replace(/\s+/g, " "); if (!dataType) return null; const defaultValue = defaultIndex >= 0 ? xuguTokenRangeText(definition, tokens, defaultIndex + 1, endIndex) : undefined; if (defaultIndex >= 0 && !defaultValue) return null; return { name, dataType, mode, ordinal, hasDefault: defaultIndex >= 0, defaultValue, }; } function xuguFunctionReturnType(definition: string, tokens: XuguRoutineToken[], startIndex: number): string | undefined { let returnIndex = -1; for (let index = startIndex; index < tokens.length; index += 1) { if (isWord(tokens[index], "AS") || isWord(tokens[index], "IS")) break; if (isWord(tokens[index], "RETURN")) { returnIndex = index; break; } } if (returnIndex < 0) return undefined; let endIndex = returnIndex + 1; let depth = 0; while (endIndex < tokens.length) { const token = tokens[endIndex]; if (token.text === "(") depth += 1; if (token.text === ")") depth = Math.max(0, depth - 1); if (depth === 0 && (isWord(token, "AS") || isWord(token, "IS") || isWord(token, "AUTHID") || isWord(token, "PIPELINED") || isWord(token, "DETERMINISTIC"))) break; endIndex += 1; } if (returnIndex + 1 >= endIndex) return undefined; return xuguTokenRangeText(definition, tokens, returnIndex + 1, endIndex).replace(/\s+/g, " ") || undefined; } function xuguTokenRangeText(definition: string, tokens: XuguRoutineToken[], startIndex: number, endIndex: number): string { if (startIndex >= endIndex) return ""; return stripXuguSqlComments(definition.slice(tokens[startIndex].start, tokens[endIndex - 1].end)).trim(); } function stripXuguSqlComments(value: string): string { let result = ""; let index = 0; let quote = ""; while (index < value.length) { const char = value[index]; if (quote) { result += char; if (char === quote) { if (value[index + 1] === quote) { result += value[index + 1]; index += 2; continue; } quote = ""; } index += 1; continue; } if (char === "'" || char === '"') { quote = char; result += char; index += 1; continue; } if (char === "-" && value[index + 1] === "-") { result += " "; index += 2; while (index < value.length && value[index] !== "\n" && value[index] !== "\r") index += 1; continue; } if (char === "/" && value[index + 1] === "*") { result += " "; const close = value.indexOf("*/", index + 2); if (close < 0) break; index = close + 2; continue; } result += char; index += 1; } return result; } function unquoteXuguIdentifier(value: string): string { if (!value.startsWith('"') || !value.endsWith('"')) return value; return value.slice(1, -1).replace(/""/g, '"'); } 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, "''")}'`; }