dbx/apps/desktop/src/lib/routineExecutionSql.ts

118 lines
5.2 KiB
TypeScript

import type { DatabaseType } from "@/types/database";
import { isSchemaAware } from "./databaseCapabilities";
import { quoteTableIdentifier } from "./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 (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;`;
}
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<RoutineParameterValue, "mode">): 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";
}