import type { DatabaseType } from "@/types/database"; import { isSchemaAware } from "@/lib/database/databaseCapabilities"; import { quoteTableIdentifier } from "@/lib/table/tableSelectSql"; export interface BuildRoutineExecutionSqlOptions { databaseType?: DatabaseType; schema?: string; routineName: string; } export type RoutineParameterMode = "IN" | "OUT" | "INOUT" | "RETURN" | "UNKNOWN"; export interface RoutineParameter { name: string; dataType: string; mode: RoutineParameterMode; ordinal: number; hasDefault?: boolean; defaultValue?: string | null; } export interface RoutineParameterValue extends RoutineParameter { value: string; useNull?: boolean; useDefault?: boolean; } export function qualifiedRoutineName(options: BuildRoutineExecutionSqlOptions): string { const { databaseType, schema, routineName } = options; if (databaseType === "databend") return routineName; if (isSchemaAware(databaseType) && schema) { return `${quoteTableIdentifier(databaseType, schema)}.${quoteTableIdentifier(databaseType, routineName)}`; } return quoteTableIdentifier(databaseType, routineName); } export function buildProcedureExecutionSql(options: BuildRoutineExecutionSqlOptions): string { return buildProcedureExecutionSqlFromValues({ ...options, parameters: [] }); } export function buildProcedureExecutionSqlFromValues(options: BuildRoutineExecutionSqlOptions & { parameters: RoutineParameterValue[] }): string { const routine = qualifiedRoutineName(options); const sortedParameters = [...options.parameters].sort((a, b) => a.ordinal - b.ordinal); 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;`; } if (options.databaseType === "databend") { return `CALL PROCEDURE ${routine}(${values.map((parameter) => routineArgumentSql(options.databaseType, parameter, useNamedArguments)).join(", ")});`; } return `CALL ${routine}(${values.map((parameter) => routineArgumentSql(options.databaseType, parameter, useNamedArguments)).join(", ")});`; } export function shouldIncludeParameter(parameter: RoutineParameterValue): boolean { if (parameter.useDefault && parameter.hasDefault) return false; return acceptsRoutineInput(parameter); } export function acceptsRoutineInput(parameter: Pick): boolean { return parameter.mode === "IN" || parameter.mode === "INOUT" || parameter.mode === "UNKNOWN"; } export function routineParameterSqlValue(databaseType: DatabaseType | undefined, parameter: RoutineParameterValue): string { if (parameter.useNull) return "NULL"; const raw = parameter.value; if (raw.trim() === "") return "NULL"; if (looksLikeNumericType(parameter.dataType)) return raw.trim(); if (looksLikeBooleanType(parameter.dataType)) return normalizeBooleanLiteral(raw, databaseType); return quoteSqlString(raw); } function sqlServerParameterName(name: string): string { return name.startsWith("@") ? name : `@${name}`; } function routineArgumentSql(databaseType: DatabaseType | undefined, parameter: RoutineParameterValue, useNamedArguments: boolean): string { const value = routineParameterSqlValue(databaseType, parameter); if (!useNamedArguments) return value; return `${quoteTableIdentifier(databaseType, parameter.name)} => ${value}`; } function shouldUseNamedArguments(databaseType: DatabaseType | undefined, sortedParameters: RoutineParameterValue[]): boolean { if (databaseType !== "postgres" && databaseType !== "oracle" && databaseType !== "dameng" && databaseType !== "oceanbase-oracle") { return false; } let omittedDefault = false; for (const parameter of sortedParameters) { if (parameter.useDefault && parameter.hasDefault && acceptsRoutineInput(parameter)) { omittedDefault = true; continue; } if (omittedDefault && shouldIncludeParameter(parameter)) return sortedParameters.every((item) => !!item.name); } return false; } function quoteSqlString(value: string): string { return `'${value.replace(/'/g, "''")}'`; } function looksLikeNumericType(dataType: string): boolean { return /\b(bigint|int|integer|smallint|tinyint|serial|number|numeric|decimal|dec|float|double|real|money)\b/i.test(dataType); } function looksLikeBooleanType(dataType: string): boolean { return /\b(bool|boolean|bit)\b/i.test(dataType); } function normalizeBooleanLiteral(value: string, databaseType: DatabaseType | undefined): string { const normalized = value.trim().toLowerCase(); const truthy = normalized === "true" || normalized === "t" || normalized === "yes" || normalized === "y" || normalized === "1"; const falsy = normalized === "false" || normalized === "f" || normalized === "no" || normalized === "n" || normalized === "0"; if (!truthy && !falsy) return quoteSqlString(value); if (databaseType === "sqlserver") return truthy ? "1" : "0"; return truthy ? "TRUE" : "FALSE"; }