Files
dbx/apps/desktop/src/lib/database/jdbcxBuiltinDriver.ts
T

115 lines
6.3 KiB
TypeScript

import type { ConnectionConfig, JdbcDriverInfo, JdbcMavenBundleInfo } from "@/types/database";
export const JDBCX_DRIVER_PROFILE = "jdbcx";
export const JDBCX_JDBC_DRIVER_CLASS = "io.github.jdbcx.WrappedDriver";
export const JDBCX_DEFAULT_URL = "jdbcx:";
export const JDBCX_HIGH_PRIVILEGE_EXTENSIONS_JAVA_OPTION = "-Ddbx.jdbcx.allowHighPrivilegeExtensions=true";
const JDBCX_HIGH_PRIVILEGE_EXTENSIONS_JAVA_OPTION_PREFIX = "-Ddbx.jdbcx.allowHighPrivilegeExtensions=";
export type JdbcxRuntimeDriverApi = {
listJdbcDrivers: () => Promise<JdbcDriverInfo[]>;
listJdbcMavenBundles: () => Promise<JdbcMavenBundleInfo[]>;
jdbcPluginStatus: () => Promise<{ installed: boolean; compatible: boolean }>;
installJdbcPlugin: () => Promise<unknown>;
};
export type JdbcxRuntimeDriverResult = {
bundles: JdbcMavenBundleInfo[];
paths: string[];
runtimeSelectionId: string;
};
type JdbcxRuntimeCandidate = {
id: string;
paths: string[];
runtimePaths: string[];
};
export function isJdbcxRuntimePath(path: string): boolean {
return /(?:^|[/\\])jdbcx-driver(?:-|\.)/i.test(path);
}
export function isJdbcxRuntimeBundle(bundle: JdbcMavenBundleInfo): boolean {
const [groupId, artifactId] = bundle.coordinate.split(":");
return groupId === "io.github.jdbcx" && artifactId === "jdbcx-driver";
}
export function jdbcxHighPrivilegeExtensionsEnabled(config: Pick<ConnectionConfig, "agent_java_options">): boolean {
const option = [...(config.agent_java_options ?? [])]
.reverse()
.map((value) => value.trim())
.find((value) => value.startsWith(JDBCX_HIGH_PRIVILEGE_EXTENSIONS_JAVA_OPTION_PREFIX));
return option?.slice(option.indexOf("=") + 1).toLowerCase() === "true";
}
export function setJdbcxHighPrivilegeExtensionsEnabled(config: Pick<ConnectionConfig, "agent_java_options">, enabled: boolean): void {
// Canonicalize this DBX-owned option so legacy whitespace cannot diverge from backend parsing.
const options = (config.agent_java_options ?? []).filter((option) => !option.trim().startsWith(JDBCX_HIGH_PRIVILEGE_EXTENSIONS_JAVA_OPTION_PREFIX));
config.agent_java_options = enabled ? [...options, JDBCX_HIGH_PRIVILEGE_EXTENSIONS_JAVA_OPTION] : options;
}
function runtimeCandidates(bundles: JdbcMavenBundleInfo[], installedDrivers: JdbcDriverInfo[], configuredPaths: string[]): JdbcxRuntimeCandidate[] {
const bundleCandidates = bundles.filter(isJdbcxRuntimeBundle).map((bundle) => {
const paths = bundle.artifacts.map((artifact) => artifact.path).filter(Boolean);
return {
id: `maven:${bundle.id}`,
paths,
runtimePaths: paths.filter(isJdbcxRuntimePath),
};
});
const mavenBundleIds = new Set(bundles.map((bundle) => bundle.id));
const installedBundlePaths = new Map<string, string[]>();
for (const driver of installedDrivers) {
if (!driver.bundle_id || mavenBundleIds.has(driver.bundle_id)) continue;
installedBundlePaths.set(driver.bundle_id, [...(installedBundlePaths.get(driver.bundle_id) ?? []), driver.path]);
}
const localBundleCandidates = [...installedBundlePaths].map(([bundleId, paths]) => ({
id: `local:${bundleId}`,
paths,
runtimePaths: paths.filter(isJdbcxRuntimePath),
}));
const standaloneCandidates = installedDrivers.filter((driver) => !driver.bundle_id && isJdbcxRuntimePath(driver.path)).map((driver) => ({ id: `manual:${driver.path}`, paths: [driver.path], runtimePaths: [driver.path] }));
const knownRuntimePaths = new Set([...bundleCandidates, ...localBundleCandidates, ...standaloneCandidates].flatMap((candidate) => candidate.runtimePaths));
const configuredCandidates = configuredPaths.filter((path) => isJdbcxRuntimePath(path) && !knownRuntimePaths.has(path)).map((path) => ({ id: `manual:${path}`, paths: [path], runtimePaths: [path] }));
return [...bundleCandidates, ...localBundleCandidates, ...standaloneCandidates, ...configuredCandidates].filter((candidate) => candidate.runtimePaths.length > 0);
}
function selectRuntimeCandidate(candidates: JdbcxRuntimeCandidate[], configuredPaths: string[]): JdbcxRuntimeCandidate {
const configured = new Set(configuredPaths);
const selected = candidates.filter((candidate) => candidate.runtimePaths.some((path) => configured.has(path)));
if (selected.length > 1) {
throw new Error("Multiple JDBCX runtimes are selected. Keep exactly one JDBCX bundle/runtime in Driver JARs, then retry.");
}
if (selected.length === 1) return selected[0];
if (candidates.length > 0) {
throw new Error("JDBCX runtime is installed but not selected. Select exactly one JDBCX bundle/runtime in Driver JARs, then retry.");
}
throw new Error("JDBCX runtime is not installed. Install io.github.jdbcx:jdbcx-driver:<version> in Driver Store, then retry.");
}
export async function ensureJdbcxRuntimeDrivers(config: ConnectionConfig, api: JdbcxRuntimeDriverApi, onInstalling?: (coordinates: string[]) => void): Promise<JdbcxRuntimeDriverResult | undefined> {
if (config.db_type !== "jdbc" || config.driver_profile !== JDBCX_DRIVER_PROFILE) return undefined;
config.connection_string = config.connection_string?.trim() || JDBCX_DEFAULT_URL;
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || JDBCX_JDBC_DRIVER_CLASS;
const configuredPaths = (config.jdbc_driver_paths ?? []).map((path) => path.trim()).filter(Boolean);
const pluginStatus = await api.jdbcPluginStatus();
if (!pluginStatus.installed || !pluginStatus.compatible) {
onInstalling?.([]);
await api.installJdbcPlugin();
}
const [bundles, installedDrivers] = await Promise.all([api.listJdbcMavenBundles(), api.listJdbcDrivers()]);
const candidates = runtimeCandidates(bundles, installedDrivers, configuredPaths);
const selectedRuntime = selectRuntimeCandidate(candidates, configuredPaths);
const installedRuntimeArtifacts = new Set(candidates.flatMap((candidate) => candidate.paths));
const selectedDriverPaths = configuredPaths.filter((path) => !installedRuntimeArtifacts.has(path));
const paths = Array.from(new Set([...selectedDriverPaths, ...selectedRuntime.paths]));
// JDBCX discovers the delegate driver through JDBC ServiceLoader/Driver.acceptsURL.
// Keep the classpath scoped to the connection-selected vendor driver and the
// JDBCX runtime so unrelated driver dependencies cannot conflict.
config.jdbc_driver_paths = paths;
return { bundles, paths, runtimeSelectionId: selectedRuntime.id };
}