feat(jdbc): add managed Apache Phoenix profile

This commit is contained in:
Guoyu Su
2026-08-06 23:16:12 +08:00
committed by GitHub
parent c835deb3f9
commit 69f4450aee
38 changed files with 1651 additions and 77 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

+18 -1
View File
@@ -104,6 +104,7 @@ import { codeMirrorSqlDialect, connectionUsesDatabaseObjectTreeMode, effectiveDa
import { sqlFormatDialectForDbType } from "@/lib/sql/sqlFormatter";
import { detectDatabaseFileType } from "@/lib/database/databaseFileDetection";
import { ensureJdbcxRuntimeDrivers } from "@/lib/database/jdbcxBuiltinDriver";
import { ensureRegisteredJdbcProductRuntimeDrivers } from "@/lib/database/jdbcProductProfiles";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@@ -136,7 +137,23 @@ const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const savedSqlStore = useSavedSqlStore();
const promptTemplateStore = usePromptTemplateStore();
connectionStore.setBeforeConnectHandler((config) => ensureJdbcxRuntimeDrivers(config, api).then(() => undefined));
connectionStore.setBeforeConnectHandler(async (config) => {
await ensureJdbcxRuntimeDrivers(config, api);
const jdbcProductRuntimeBefore = JSON.stringify({
connectionString: config.connection_string ?? null,
driverClass: config.jdbc_driver_class ?? null,
driverPaths: config.jdbc_driver_paths ?? [],
});
const jdbcProductRuntime = await ensureRegisteredJdbcProductRuntimeDrivers(config, api);
const jdbcProductRuntimeAfter = JSON.stringify({
connectionString: config.connection_string ?? null,
driverClass: config.jdbc_driver_class ?? null,
driverPaths: config.jdbc_driver_paths ?? [],
});
if (jdbcProductRuntime && jdbcProductRuntimeBefore !== jdbcProductRuntimeAfter && connectionStore.getConfig(config.id)) {
await connectionStore.updateConnection(config);
}
});
const { message: toastMessage, visible: toastVisible, toast } = useToast();
const { isDark, themeMode, applyTheme, setThemeMode } = useTheme();
const { activeCount: activeBackgroundTaskCount } = useExportTracker();
@@ -30,7 +30,7 @@ import {
updatePerDriverProgress,
type DriverInstallProgress,
} from "@/lib/connection/driverInstallProgressUi";
import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
import { installRegisteredManagedJdbcDriver, isManagedJdbcDriver, managedJdbcDriverRows, uninstallRegisteredManagedJdbcDriver } from "@/lib/database/managedJdbcDrivers";
import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
import { translateBackendError } from "@/i18n/backend-errors";
@@ -347,11 +347,11 @@ function getJreReinstallTitle(fallback: string): string {
return formatProgressText(jreReinstallProgress.value) || fallback;
}
function isPrestoSqlBuiltinDriver(dbType: string): boolean {
return dbType === PRESTOSQL_DRIVER_DB_TYPE;
function isManagedJdbcBuiltinDriver(dbType: string): boolean {
return isManagedJdbcDriver(dbType);
}
const builtinDriverRows = computed<AgentDriverInfo[]>(() => [...drivers.value, prestoSqlBuiltinDriverRow(jdbcMavenBundles.value)]);
const builtinDriverRows = computed<AgentDriverInfo[]>(() => [...drivers.value, ...managedJdbcDriverRows(jdbcMavenBundles.value, jdbcPluginStatus.value)]);
function driverLabel(dbType: string): string {
return builtinDriverRows.value.find((d) => d.db_type === dbType)?.label ?? dbType;
@@ -469,13 +469,14 @@ async function runDriverInstall(dbType: string) {
activeAgentOperationId.value = uuid();
resetAgentInstallProgress();
try {
if (isPrestoSqlBuiltinDriver(dbType)) {
if (!jdbcPluginStatus.value?.installed || !jdbcPluginStatus.value.compatible) {
jdbcPluginStatus.value = await api.installJdbcPlugin();
const managedResult = await installRegisteredManagedJdbcDriver(dbType, jdbcMavenBundles.value, jdbcPluginStatus.value, api);
if (managedResult) {
if (managedResult.pluginStatus) {
jdbcPluginStatus.value = managedResult.pluginStatus;
emitDriverUpdateCount();
}
jdbcDrivers.value = await api.installPrestoSqlJdbcDriver();
jdbcMavenBundles.value = await api.listJdbcMavenBundles();
if (managedResult.drivers) jdbcDrivers.value = managedResult.drivers;
jdbcMavenBundles.value = managedResult.bundles;
void loadDriverStoreUsage();
toast(t("driverStore.driverInstallSuccess", { label }));
return;
@@ -544,11 +545,10 @@ async function upgradeAll() {
async function uninstallDriver(dbType: string) {
const label = driverLabel(dbType);
try {
if (isPrestoSqlBuiltinDriver(dbType)) {
const bundle = prestoSqlMavenBundle(jdbcMavenBundles.value);
if (!bundle) return;
jdbcDrivers.value = await api.deleteJdbcMavenBundle(bundle.id);
jdbcMavenBundles.value = await api.listJdbcMavenBundles();
const managedResult = await uninstallRegisteredManagedJdbcDriver(dbType, jdbcMavenBundles.value, api);
if (managedResult) {
if (managedResult.drivers) jdbcDrivers.value = managedResult.drivers;
jdbcMavenBundles.value = managedResult.bundles;
void loadDriverStoreUsage();
toast(t("driverStore.driverUninstallSuccess", { label }));
return;
@@ -642,7 +642,7 @@ async function importOfflineZip() {
async function importDriverFile(driver: AgentDriverInfo) {
if (agentImportBusy.value) return;
const dbType = driver.db_type;
if (isPrestoSqlBuiltinDriver(dbType)) {
if (isManagedJdbcBuiltinDriver(dbType)) {
await importJdbcDrivers();
return;
}
@@ -1267,11 +1267,11 @@ onMounted(async () => {
const incoming = payload as DriverInstallProgress;
if (!isDriverInstallProgressForOperation(incoming, activeAgentOperationId.value)) return;
const channel = driverInstallProgressChannel(incoming);
const jdbcProgressBelongsToPrestoSql = channel === "jdbc-plugin" && installing.value === PRESTOSQL_DRIVER_DB_TYPE && !isInstallingJdbcPlugin.value;
if (jdbcProgressBelongsToPrestoSql) {
// PrestoSQL is shown as a built-in driver but installs through the JDBC plugin pipeline.
// Route its events into the per-driver map using the presto key.
updatePerDriverProgress(agentProgressByDbType, { ...incoming, db_type: PRESTOSQL_DRIVER_DB_TYPE });
const jdbcProgressBuiltinDriver = channel === "jdbc-plugin" && installing.value && isManagedJdbcBuiltinDriver(installing.value) && !isInstallingJdbcPlugin.value ? installing.value : null;
if (jdbcProgressBuiltinDriver) {
// Built-in JDBC profiles are shown alongside Agent drivers but install
// through the JDBC plugin pipeline. Route plugin events into their row.
updatePerDriverProgress(agentProgressByDbType, { ...incoming, db_type: jdbcProgressBuiltinDriver });
} else if (channel === "agent") {
if (incoming.db_type) {
updatePerDriverProgress(agentProgressByDbType, incoming);
@@ -1473,7 +1473,7 @@ watch(driverStoreTab, (tab) => {
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
variant="ghost"
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
@@ -1591,7 +1591,7 @@ watch(driverStoreTab, (tab) => {
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
variant="ghost"
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
@@ -1675,7 +1675,7 @@ watch(driverStoreTab, (tab) => {
{{ t("driverStore.install") }}
</Button>
<Button
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
v-if="!driver.installed && !isManagedJdbcBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
size="sm"
variant="ghost"
class="h-7 w-7 rounded-md text-xs text-muted-foreground"
@@ -107,6 +107,28 @@ import { hasCloudflareD1Credentials, isCloudflareD1Connection, normalizeCloudfla
import { buildElasticsearchExternalConfig, elasticsearchConnectionModeFromConfig, elasticsearchConnectivityCheckPathFromConfig, elasticsearchKibanaBasePathFromConfig, type ElasticsearchConnectionMode } from "@/lib/connection/elasticsearchKibanaProxy";
import { GAUSSDB_M_JDBC_DRIVER_CLASS, gaussdbConnectionMode, gaussdbIdentifierQuoteStyle, setGaussdbConnectionMode, setGaussdbIdentifierQuoteStyle, supportsGaussdbIdentifierQuoteStyle, type GaussdbConnectionMode, type GaussdbIdentifierQuoteStyle } from "@/lib/database/jdbcDialect";
import { normalizeStoredConnectionDatabase } from "@/lib/database/sqliteNamespace";
import {
createJdbcProductConnectionFieldsByMode,
isJdbcProductDefaultDriverClass,
isJdbcProductManagedMavenCoordinate,
isJdbcProductManagedMavenPath,
jdbcProductConnectionDefaults,
jdbcProductManagedRuntimePaths,
jdbcProductMode,
jdbcProductRuntimeSelectionId,
rememberJdbcProductConnectionFields,
type JdbcProductConnectionFieldsByMode,
} from "@/lib/database/jdbcProductProfile";
import {
ensureRegisteredJdbcProductRuntimeDrivers,
isRegisteredJdbcProductRuntimeInstallError,
jdbcProductDriverProfiles,
jdbcProductIconTypes,
jdbcProductPickerOptions,
jdbcProductProfileDefinition,
jdbcProductProfileForConfig,
jdbcProductProfileIdsForCategory,
} from "@/lib/database/jdbcProductProfiles";
type DbOption = { value: string; label: string };
type DbCategoryKey = "sql" | "analytics" | "domestic" | "lightweight" | "document" | "graph_ai" | "timeseries" | "mq" | "registry_config";
@@ -123,6 +145,7 @@ type JdbcDriverSelectItem = {
label: string;
paths: string[];
jdbcxRuntime: boolean;
managedProductRuntime?: boolean;
};
const DREMIO_ARROW_FLIGHT_SQL_JDBC_URL = "jdbc:arrow-flight-sql://127.0.0.1:32010";
@@ -197,6 +220,7 @@ const agentInstallLabel = ref("");
const agentInstallProgress = ref<DriverInstallProgress | null>(null);
const agentInstallError = ref("");
const showConnectionErrorDialog = ref(false);
const connectionErrorRawDetail = ref("");
const connectionErrorDetail = ref("");
const editingId = ref<string | null>(null);
const draftTestConnectionId = ref(uuid());
@@ -535,7 +559,7 @@ function resizeNoteTextarea() {
textarea.style.overflowY = textarea.scrollHeight > maxContentHeight ? "auto" : "hidden";
}
const showJdbcDependencyDriverManagerAction = computed(() => form.value.db_type === "jdbc" && isJdbcMissingRuntimeDependencyError(connectionErrorDetail.value));
const showJdbcDependencyDriverManagerAction = computed(() => form.value.db_type === "jdbc" && (isJdbcMissingRuntimeDependencyError(connectionErrorRawDetail.value) || isRegisteredJdbcProductRuntimeInstallError(form.value, connectionErrorRawDetail.value)));
function externalConfigRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record<string, unknown>) } : {};
@@ -597,6 +621,13 @@ const dremioConnectionUrls = ref<Record<DremioConnectionMode, string>>({
"arrow-flight-sql": DREMIO_ARROW_FLIGHT_SQL_JDBC_URL,
legacy: DREMIO_LEGACY_JDBC_URL,
});
const jdbcProductConnectionMode = ref("");
const jdbcProductConnectionFields = ref<JdbcProductConnectionFieldsByMode>({});
const activeJdbcProductProfile = computed(() => jdbcProductProfileForConfig(form.value));
const activeJdbcProductMode = computed(() => {
const profile = activeJdbcProductProfile.value;
return profile ? jdbcProductMode(profile, jdbcProductConnectionMode.value) : undefined;
});
const hiveAuthMode = ref<HiveKerberosAuthMode>("none");
const hivePrincipal = ref("");
const hiveKrb5ConfPath = ref("");
@@ -787,12 +818,16 @@ const jdbcDriverSelectItems = computed<JdbcDriverSelectItem[]>(() => {
paths: bundle.artifacts.map((artifact) => artifact.path),
jdbcxRuntime: bundle.artifacts.some((artifact) => isJdbcxRuntimePath(artifact.path)),
}));
const bundles = jdbcMavenBundles.value.map((bundle) => ({
id: `maven:${bundle.id}`,
label: bundle.coordinate,
paths: bundle.artifacts.map((artifact) => artifact.path),
jdbcxRuntime: isJdbcxRuntimeBundle(bundle),
}));
const productProfile = activeJdbcProductProfile.value;
const productMode = activeJdbcProductMode.value;
const bundles = jdbcMavenBundles.value
.filter((bundle) => !productProfile || !isJdbcProductManagedMavenCoordinate(productProfile, bundle.coordinate))
.map((bundle) => ({
id: `maven:${bundle.id}`,
label: bundle.coordinate,
paths: bundle.artifacts.map((artifact) => artifact.path),
jdbcxRuntime: isJdbcxRuntimeBundle(bundle),
}));
const manual = jdbcDrivers.value
.filter((driver) => !driver.bundle_id)
.map((driver) => ({
@@ -801,7 +836,22 @@ const jdbcDriverSelectItems = computed<JdbcDriverSelectItem[]>(() => {
paths: [driver.path],
jdbcxRuntime: isJdbcxRuntimePath(driver.path),
}));
return [...localBundles, ...bundles, ...manual].sort((left, right) => left.label.localeCompare(right.label));
const productPaths = productProfile && productMode ? jdbcProductManagedRuntimePaths(productProfile, jdbcMavenBundles.value, productMode.id) : [];
const managedProductRuntime: JdbcDriverSelectItem[] =
productProfile && productMode && productPaths.length > 0
? [
{
id: jdbcProductRuntimeSelectionId(productProfile, productMode.id),
label: t(productProfile.runtimeLabelKey, {
mode: t(productMode.labelKey),
}),
paths: productPaths,
jdbcxRuntime: false,
managedProductRuntime: true,
},
]
: [];
return [...managedProductRuntime, ...localBundles, ...bundles, ...manual].sort((left, right) => left.label.localeCompare(right.label));
});
const jdbcDriverSelectItemById = computed(() => new Map(jdbcDriverSelectItems.value.map((item) => [item.id, item])));
@@ -1025,6 +1075,7 @@ const driverProfiles: Record<
icon: "postgres",
urlParams: "",
},
...jdbcProductDriverProfiles(),
};
function profileForConfig(config: ConnectionConfig) {
@@ -1604,6 +1655,7 @@ function failAgentDriverInstall(error: unknown) {
}
function showConnectionError(message: string) {
connectionErrorRawDetail.value = message;
connectionErrorDetail.value = translateBackendError(t, message);
showConnectionErrorDialog.value = true;
}
@@ -1677,6 +1729,19 @@ async function ensureRequiredJdbcxDriverInstalled(config: ConnectionConfig): Pro
}
}
async function ensureRequiredJdbcProductRuntimeInstalled(config: ConnectionConfig): Promise<void> {
const result = await ensureRegisteredJdbcProductRuntimeDrivers(config, api);
if (!result) return;
jdbcMavenBundles.value = result.bundles;
jdbcDriverPathsInput.value = result.paths.join("\n");
form.value.jdbc_driver_paths = [...result.paths];
selectedJdbcDriverPath.value = result.runtimeSelectionId ?? "";
if (result.paths.length > 0) {
jdbcManualClasspathOpen.value = false;
}
}
async function ensureRequiredGaussdbMJdbcRuntime(config: ConnectionConfig): Promise<void> {
if (gaussdbConnectionMode(config) !== "m-jdbc") return;
if (!(config.jdbc_driver_paths ?? []).length) {
@@ -1964,6 +2029,64 @@ function dremioConnectionModeForConfig(config: Pick<ConnectionConfig, "connectio
return haystack.includes("jdbc:dremio:") || haystack.includes("com.dremio.jdbc.driver") ? "legacy" : "arrow-flight-sql";
}
function currentJdbcProductConnectionFields() {
return {
connectionString: form.value.connection_string || "",
driverClass: form.value.jdbc_driver_class || "",
};
}
function applyJdbcProductConnectionMode(modeId: string) {
const profile = activeJdbcProductProfile.value;
if (!profile) return;
jdbcProductConnectionFields.value = rememberJdbcProductConnectionFields(profile, jdbcProductConnectionFields.value, jdbcProductConnectionMode.value, currentJdbcProductConnectionFields());
jdbcProductConnectionMode.value = modeId;
const fields = jdbcProductConnectionFields.value[modeId] ?? jdbcProductConnectionDefaults(profile, modeId);
form.value.connection_string = fields.connectionString;
form.value.jdbc_driver_class = fields.driverClass;
resetTestState();
}
function resetJdbcProductConnectionFields(profile = activeJdbcProductProfile.value, config?: Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">) {
if (!profile) {
jdbcProductConnectionMode.value = "";
jdbcProductConnectionFields.value = {};
return;
}
jdbcProductConnectionMode.value = config ? profile.detectMode(config) : jdbcProductMode(profile, "").id;
jdbcProductConnectionFields.value = createJdbcProductConnectionFieldsByMode(profile, config);
}
function restoreJdbcProductConnectionDefaultsIfEmpty() {
const profile = activeJdbcProductProfile.value;
if (!profile) return;
const defaults = jdbcProductConnectionDefaults(profile, jdbcProductConnectionMode.value);
form.value.connection_string = form.value.connection_string?.trim() || defaults.connectionString;
form.value.jdbc_driver_class = form.value.jdbc_driver_class?.trim() || defaults.driverClass;
}
function syncJdbcProductConnectionModeFromUrl() {
const profile = activeJdbcProductProfile.value;
if (!profile) return;
restoreJdbcProductConnectionDefaultsIfEmpty();
const nextMode = profile.detectMode(form.value);
if (nextMode === jdbcProductConnectionMode.value) {
jdbcProductConnectionFields.value = rememberJdbcProductConnectionFields(profile, jdbcProductConnectionFields.value, nextMode, currentJdbcProductConnectionFields());
return;
}
if (isJdbcProductDefaultDriverClass(profile, form.value.jdbc_driver_class)) {
form.value.jdbc_driver_class = jdbcProductConnectionDefaults(profile, nextMode).driverClass;
}
jdbcProductConnectionMode.value = nextMode;
jdbcProductConnectionFields.value = rememberJdbcProductConnectionFields(profile, jdbcProductConnectionFields.value, nextMode, currentJdbcProductConnectionFields());
}
function syncJdbcProfileModeFromUrl() {
syncDremioConnectionModeFromUrl();
syncJdbcProductConnectionModeFromUrl();
}
function isCustomCompatibleProfile() {
return selectedType.value === "custom_mysql" || selectedType.value === "custom_postgres";
}
@@ -2016,6 +2139,12 @@ function applyProfile(val: string, preserveConnectionFields = false) {
if (val === "dremio") {
resetDremioConnectionUrls();
applyDremioConnectionMode("legacy");
} else if (jdbcProductProfileDefinition(val)) {
const jdbcProductProfile = jdbcProductProfileDefinition(val)!;
resetJdbcProductConnectionFields(jdbcProductProfile);
const fields = jdbcProductConnectionDefaults(jdbcProductProfile, jdbcProductConnectionMode.value);
form.value.connection_string = fields.connectionString;
form.value.jdbc_driver_class = fields.driverClass;
} else if (val === JDBCX_DRIVER_PROFILE) {
form.value.connection_string = JDBCX_DEFAULT_URL;
form.value.jdbc_driver_class = JDBCX_JDBC_DRIVER_CLASS;
@@ -2195,6 +2324,7 @@ watch(
}
dremioConnectionMode.value = profile === "dremio" ? dremioConnectionModeForConfig(config) : "legacy";
resetDremioConnectionUrls(dremioConnectionMode.value, profile === "dremio" ? config.connection_string : undefined);
resetJdbcProductConnectionFields(jdbcProductProfileForConfig(config), config);
mongoUseUrl.value = !!config.connection_string;
jdbcDriverPathsInput.value = (config.jdbc_driver_paths || []).join("\n");
jdbcManualClasspathOpen.value = config.db_type === "prestosql" || (config.jdbc_driver_paths || []).length > 0;
@@ -2225,6 +2355,7 @@ watch(
h2ConnectionMode.value = "file";
dremioConnectionMode.value = "legacy";
resetDremioConnectionUrls();
resetJdbcProductConnectionFields(undefined);
dialogStep.value = "select";
configTab.value = "connection";
}
@@ -2443,6 +2574,7 @@ const iconTypeMap: Record<string, string> = {
jdbc: "jdbc",
custom_mysql: "mysql",
custom_postgres: "postgres",
...jdbcProductIconTypes(),
};
const dbOptions: DbOption[] = [
@@ -2530,12 +2662,13 @@ const dbOptions: DbOption[] = [
{ value: "custom_mysql", label: "Custom (MySQL)" },
{ value: "custom_postgres", label: "Custom (PostgreSQL)" },
{ value: "dremio", label: "Dremio" },
...jdbcProductPickerOptions(),
];
const dbCategoryDefinitions: Array<{
key: DbCategoryKey;
titleKey: string;
optionValues: readonly string[];
optionValues: string[];
}> = [
{
key: "sql",
@@ -2584,6 +2717,10 @@ const dbCategoryDefinitions: Array<{
},
];
for (const category of dbCategoryDefinitions) {
category.optionValues.push(...jdbcProductProfileIdsForCategory(category.key));
}
// Keep the picker exhaustive as database drivers are added or reorganized.
assertCompleteDatabaseCategories(
dbOptions.map((option) => option.value),
@@ -2649,6 +2786,7 @@ const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selecte
const jdbcBackedDatabaseTypes = new Set<DatabaseType>(["jdbc", "prestosql"]);
const isJdbcConnection = computed(() => form.value.db_type === "jdbc");
const isJdbcxConnection = computed(() => isJdbcConnection.value && form.value.driver_profile === JDBCX_DRIVER_PROFILE);
const isJdbcProductConnection = computed(() => Boolean(activeJdbcProductProfile.value));
const jdbcxHighPrivilegeExtensionsAllowed = computed({
get: () => jdbcxHighPrivilegeExtensionsEnabled(form.value),
set: (enabled: boolean) => {
@@ -2661,7 +2799,7 @@ const isH2FileMode = computed(() => form.value.db_type === "h2" && h2ConnectionM
const usesLocalFilePathInput = computed(() => isLocalFileTypeDb(form.value.db_type) && (form.value.db_type !== "h2" || isH2FileMode.value));
const connectionUrlPlaceholder = computed(() => getUrlPlaceholder(form.value.db_type));
const jdbcUsernamePlaceholder = computed(() => (form.value.driver_profile === "dremio" ? "" : "sa"));
const jdbcUsernamePlaceholder = computed(() => (form.value.driver_profile === "dremio" || isJdbcProductConnection.value ? "" : "sa"));
const filePathPlaceholder = computed(() => {
if (form.value.db_type === "duckdb") return "/path/to/database.duckdb or :memory:";
if (form.value.db_type === "access") return "/path/to/database.accdb";
@@ -3104,6 +3242,7 @@ async function testConnection() {
await ensureRequiredAgentDriverInstalled(config);
await ensureRequiredGaussdbMJdbcRuntime(config);
await ensureRequiredJdbcxDriverInstalled(config);
await ensureRequiredJdbcProductRuntimeInstalled(config);
const result = await testConnectionWithTimeout(config, runId);
if (runId !== testRunId) return;
let successfulConfig = config;
@@ -3566,6 +3705,13 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
if (config.db_type === "jdbc") {
if (config.driver_profile === "dremio") {
applyDremioJdbcMetadata(config);
} else if (jdbcProductProfileForConfig(config)) {
const jdbcProductProfile = jdbcProductProfileForConfig(config)!;
const defaults = jdbcProductConnectionDefaults(jdbcProductProfile, jdbcProductProfile.detectMode(config));
config.host = "";
config.port = 0;
config.connection_string = config.connection_string?.trim() || defaults.connectionString;
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || defaults.driverClass;
} else if (config.driver_profile === JDBCX_DRIVER_PROFILE) {
config.host = "";
config.port = 0;
@@ -3848,6 +3994,7 @@ function resetTestState() {
testResult.value = null;
clearTestedConnectionInfo();
showConnectionErrorDialog.value = false;
connectionErrorRawDetail.value = "";
connectionErrorDetail.value = "";
}
@@ -4247,7 +4394,7 @@ async function copyConnectionErrorDetail() {
}
function openJdbcDriverManager() {
emit("openDriverStore", { target: "tab", tab: "jdbc" });
emit("openDriverStore", isJdbcProductConnection.value ? agentDriverFocus.value : { target: "tab", tab: "jdbc" });
}
function openJdbcDriverManagerFromError() {
@@ -4267,6 +4414,7 @@ function resetForm() {
oceanbaseSubMode.value = "mysql";
dremioConnectionMode.value = "legacy";
resetDremioConnectionUrls();
resetJdbcProductConnectionFields(undefined);
jdbcDriverPathsInput.value = "";
selectedJdbcDriverPath.value = "";
connectionUrlInput.value = "";
@@ -4326,6 +4474,7 @@ function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) {
if (form.value.db_type === "h2") {
h2ConnectionMode.value = h2ConnectionModeForConfig(form.value);
}
resetJdbcProductConnectionFields(jdbcProductProfileForConfig(form.value), form.value);
if (draft.driverProfile === "oceanbase-oracle") {
oceanbaseSubMode.value = "oracle";
selectedType.value = "oceanbase";
@@ -4897,7 +5046,6 @@ async function browseJdbcDriverPaths() {
}
async function loadJdbcDrivers() {
if (!isDesktop) return;
try {
const [drivers, bundles, localBundles] = await Promise.all([api.listJdbcDrivers(), api.listJdbcMavenBundles(), api.listJdbcLocalBundles()]);
jdbcDrivers.value = drivers;
@@ -4958,7 +5106,13 @@ function onJdbcDriverSelect(id: any) {
const item = jdbcDriverSelectItemById.value.get(id);
if (!item) return;
selectedJdbcDriverPath.value = id;
if (isJdbcxConnection.value && item.jdbcxRuntime) {
if (item.managedProductRuntime && activeJdbcProductProfile.value) {
const existingPaths = jdbcDriverPathsInput.value
.split(/\r?\n/)
.map((path) => path.trim())
.filter((path) => path && !isJdbcProductManagedMavenPath(activeJdbcProductProfile.value!, path));
jdbcDriverPathsInput.value = Array.from(new Set([...existingPaths, ...item.paths])).join("\n");
} else if (isJdbcxConnection.value && item.jdbcxRuntime) {
const installedJdbcxRuntimePaths = new Set(jdbcDriverSelectItems.value.filter((candidate) => candidate.jdbcxRuntime).flatMap((candidate) => candidate.paths));
const existingPaths = jdbcDriverPathsInput.value
.split(/\r?\n/)
@@ -5281,9 +5435,27 @@ function openExternalUrl(url: string) {
</Button>
</div>
</div>
<div v-if="activeJdbcProductProfile && activeJdbcProductProfile.modes.length > 1" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.mode") }}</Label>
<div class="col-span-3 flex gap-2">
<Button v-for="mode in activeJdbcProductProfile.modes" :key="mode.id" type="button" size="sm" :variant="jdbcProductConnectionMode === mode.id ? 'default' : 'outline'" @click="applyJdbcProductConnectionMode(mode.id)">
{{ t(mode.labelKey) }}
</Button>
</div>
</div>
<div v-if="activeJdbcProductProfile && activeJdbcProductMode" class="grid grid-cols-4 items-start gap-4">
<span />
<div class="col-span-3 space-y-1 text-xs text-muted-foreground">
<p>{{ t(activeJdbcProductMode.hintKey) }}</p>
<p>
{{ t(activeJdbcProductProfile.driverManagerHintPrefixKey) }}<a class="underline cursor-pointer text-primary hover:text-primary/80" @click="emit('openDriverStore', agentDriverFocus)">{{ t("toolbar.driverManager") }}</a
>{{ t(activeJdbcProductProfile.driverManagerHintSuffixKey) }}
</p>
</div>
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.jdbcUrl") }}</Label>
<Input v-model="form.connection_string" class="col-span-3" :placeholder="t('connection.jdbcUrlPlaceholder')" @blur="syncDremioConnectionModeFromUrl" />
<Input v-model="form.connection_string" class="col-span-3" :placeholder="t('connection.jdbcUrlPlaceholder')" @blur="syncJdbcProfileModeFromUrl" />
</div>
<div v-if="isJdbcxConnection" class="grid grid-cols-4 items-start gap-4">
<Label :class="connectionLabelTopClass">{{ t("connection.jdbcxExtensions") }}</Label>
@@ -5349,17 +5521,17 @@ function openExternalUrl(url: string) {
<div class="grid grid-cols-4 items-start gap-4">
<span />
<div class="col-span-3 space-y-2">
<p class="text-xs text-muted-foreground">
<p v-if="!isJdbcProductConnection" class="text-xs text-muted-foreground">
{{ t("connection.jdbcPluginHint") }}
</p>
<div class="flex flex-wrap gap-2">
<Button type="button" variant="outline" size="sm" @click="emit('openDriverStore', { target: 'tab', tab: 'jdbc' })">
<Button type="button" variant="outline" size="sm" @click="openJdbcDriverManager">
<FolderOpen class="h-3.5 w-3.5" />
{{ t("toolbar.driverManager") }}
</Button>
<Button type="button" variant="outline" size="sm" @click="openExternalUrl('https://dbxio.com')">
<Button type="button" variant="outline" size="sm" @click="openExternalUrl(activeJdbcProductProfile?.docsUrl || 'https://dbxio.com')">
<ExternalLink class="h-3.5 w-3.5" />
{{ t("connection.jdbcDocs") }}
{{ activeJdbcProductProfile ? t(activeJdbcProductProfile.docsLabelKey) : t("connection.jdbcDocs") }}
</Button>
</div>
</div>
+11
View File
@@ -19,4 +19,15 @@ describe("DatabaseIcon", () => {
expect(container.querySelector("img")?.getAttribute("src")).toBe("/icons/database/oceanbase.svg");
app.unmount();
});
it("uses the Apache Phoenix asset for the Phoenix JDBC profile", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
const app = createApp(DatabaseIcon, { dbType: "phoenix" });
app.mount(container);
await nextTick();
expect(container.querySelector("img")?.getAttribute("src")).toBe("/icons/database/phoenix.svg");
app.unmount();
});
});
@@ -55,6 +55,7 @@ const assetIcons: Record<string, string> = {
prestosql: "presto",
hive: "hive",
hbase: "hbase",
phoenix: "phoenix",
spark: "spark-logo.png",
apache_kylin: "apache_kylin",
sundb: "sundb",
+11
View File
@@ -11,6 +11,7 @@ import ko from "@/i18n/locales/ko";
import ptBR from "@/i18n/locales/pt-BR";
import zhCN from "@/i18n/locales/zh-CN";
import zhTW from "@/i18n/locales/zh-TW";
import { PHOENIX_DRIVER_NOT_INSTALLED_ERROR, PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR } from "@/lib/database/phoenixConnection";
const LOCALES = {
en,
@@ -54,6 +55,16 @@ const WINDOWS_JRE_REMOVE_ERROR = [
// Every backend message changed away from hardcoded Chinese, paired with the
// key and params it must resolve to.
const CASES: { name: string; message: string; key: string; params?: Record<string, string> }[] = [
{
name: "Apache Phoenix JDBC driver missing",
message: PHOENIX_DRIVER_NOT_INSTALLED_ERROR,
key: "connection.phoenixDriverNotInstalled",
},
{
name: "shared JDBC plugin missing for Apache Phoenix",
message: PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR,
key: "connection.phoenixDriverNotInstalled",
},
{
name: "SSH TOTP prompt cancelled",
message: "SSH layer 1 failed: SSH keyboard-interactive authentication was cancelled",
+9
View File
@@ -1,4 +1,5 @@
import { normalizeBackendError, sanitizeBackendErrorMessage, type BackendError } from "@/lib/backend/errorUtils";
import { PHOENIX_DRIVER_NOT_INSTALLED_ERROR, PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR } from "@/lib/database/phoenixConnection";
/**
* Minimal shape of a translate function, satisfied by both `useI18n().t` inside
@@ -34,6 +35,11 @@ const taggedAiCliErrorKeys: Record<string, string> = {
piAgentRunFailed: "ai.cliErrors.piAgentRunFailed",
};
const exactMessageKeys: Record<string, string> = {
[PHOENIX_DRIVER_NOT_INSTALLED_ERROR]: "connection.phoenixDriverNotInstalled",
[PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR]: "connection.phoenixDriverNotInstalled",
};
const patterns: [RegExp, string][] = [
[/^(.+?) driver is not installed\. Please install it from the Driver Manager\.$/, "connection.driverNotInstalled"],
[/^JRE (.+?) runtime is not installed\. Please install it from the Driver Manager\.$/, "connection.jreNotInstalled"],
@@ -139,6 +145,9 @@ export function translateBackendError(t: BackendErrorTranslate, error: unknown):
if (structured) return translateStructuredBackendError(t, structured);
const message = backendErrorMessage(error);
const exactKey = exactMessageKeys[message];
if (exactKey) return t(exactKey);
const tagged = message.match(/^\[([A-Za-z][A-Za-z0-9]+)\]\s*([\s\S]*)$/);
if (tagged) {
const [, code, rawDetail] = tagged;
+9
View File
@@ -664,6 +664,7 @@ export default {
connectSuccess: "Connected to {name}",
connectFailed: "Connection failed: {message}",
driverNotInstalled: "{driver} driver is not installed. Please install it from the Driver Manager.",
phoenixDriverNotInstalled: "The Apache Phoenix JDBC driver is not installed. Install it from the Driver Manager, then retry.",
jreNotInstalled: "JRE {jre} runtime is not installed. Please install it from the Driver Manager.",
systemJavaNotFound: "System Java runtime was not found on PATH. Please install Java or choose a custom Java executable.",
customJavaPathEmpty: "Custom Java runtime path is empty. Please choose a Java executable.",
@@ -798,6 +799,14 @@ export default {
httpTunnelInvalidTimeout: "{hop}: HTTP tunnel timeout must be between 1 and 300 seconds",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "Legacy JDBC",
phoenixDirectMode: "Direct",
phoenixRuntimeOption: "Apache Phoenix JDBC ({mode})",
phoenixQueryServerMode: "Query Server",
phoenixDirectModeHint: "Connects directly to HBase. The default runtime targets Phoenix 5.2.1 with HBase 2.5; for other cluster versions, select a compatible Phoenix client and add hbase-site.xml when required.",
phoenixQueryServerModeHint: "Connects over HTTP through a separately deployed Phoenix Query Server (PQS) using the Phoenix thin client.",
phoenixDriverManagerHintPrefix: "Install or uninstall the default runtime in ",
phoenixDriverManagerHintSuffix: ". Manually selected compatible driver paths take precedence.",
phoenixDocs: "View Phoenix docs",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "Driver Class (optional)",
+9
View File
@@ -485,6 +485,7 @@ export default withEnglishFallback({
connectSuccess: "Conectado a {name}",
connectFailed: "Error de conexión: {message}",
driverNotInstalled: "El controlador {driver} no está instalado. Instálelo desde el Administrador de controladores.",
phoenixDriverNotInstalled: "El controlador JDBC de Apache Phoenix no está instalado. Instálelo desde el Administrador de controladores y vuelva a intentarlo.",
jreNotInstalled: "El entorno JRE {jre} no está instalado. Instálelo desde el Administrador de controladores.",
systemJavaNotFound: "No se encontró Java en el PATH del sistema. Instale Java o elija un ejecutable personalizado.",
customJavaPathEmpty: "La ruta de Java personalizada está vacía. Elija un ejecutable de Java.",
@@ -591,6 +592,14 @@ export default withEnglishFallback({
tunnelProfileManage: "Gestionar",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "JDBC heredado",
phoenixDirectMode: "Directo",
phoenixRuntimeOption: "Apache Phoenix JDBC ({mode})",
phoenixQueryServerMode: "Query Server",
phoenixDirectModeHint: "Se conecta directamente a HBase. El runtime predeterminado está dirigido a Phoenix 5.2.1 con HBase 2.5; para otras versiones del clúster, selecciona un cliente Phoenix compatible y añade hbase-site.xml cuando sea necesario.",
phoenixQueryServerModeHint: "Se conecta por HTTP a un Phoenix Query Server (PQS) desplegado por separado mediante el cliente ligero de Phoenix.",
phoenixDriverManagerHintPrefix: "Instala o desinstala el runtime predeterminado en ",
phoenixDriverManagerHintSuffix: ". Las rutas de drivers compatibles seleccionadas manualmente tienen prioridad.",
phoenixDocs: "Ver documentación de Phoenix",
jdbcUrl: "URL JDBC",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/base_de_datos",
jdbcDriverClass: "Clase del driver (opcional)",
+9
View File
@@ -484,6 +484,7 @@ export default withEnglishFallback({
connectSuccess: "Connesso a {name}",
connectFailed: "Connessione non riuscita: {message}",
driverNotInstalled: "Il driver {driver} non è installato. Installalo da Gestione Driver.",
phoenixDriverNotInstalled: "Il driver JDBC di Apache Phoenix non è installato. Installalo da Gestione Driver e riprova.",
jreNotInstalled: "Il runtime JRE {jre} non è installato. Installalo da Gestione Driver.",
systemJavaNotFound: "Runtime Java di sistema non trovato in PATH. Installa Java o scegli un eseguibile Java personalizzato.",
customJavaPathEmpty: "Il percorso del runtime Java personalizzato è vuoto. Scegli un eseguibile Java.",
@@ -589,6 +590,14 @@ export default withEnglishFallback({
tunnelProfileManage: "Gestisci",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "JDBC legacy",
phoenixDirectMode: "Diretta",
phoenixRuntimeOption: "Apache Phoenix JDBC ({mode})",
phoenixQueryServerMode: "Query Server",
phoenixDirectModeHint: "Si connette direttamente a HBase. Il runtime predefinito è destinato a Phoenix 5.2.1 con HBase 2.5; per altre versioni del cluster, seleziona un client Phoenix compatibile e aggiungi hbase-site.xml quando necessario.",
phoenixQueryServerModeHint: "Si connette tramite HTTP a un Phoenix Query Server (PQS) distribuito separatamente usando il client thin di Phoenix.",
phoenixDriverManagerHintPrefix: "Installa o disinstalla il runtime predefinito in ",
phoenixDriverManagerHintSuffix: ". I percorsi dei driver compatibili selezionati manualmente hanno la precedenza.",
phoenixDocs: "Visualizza la documentazione di Phoenix",
jdbcUrl: "URL JDBC",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "Classe Driver (opzionale)",
+9
View File
@@ -478,6 +478,7 @@ export default withEnglishFallback({
connectSuccess: "{name} に接続しました",
connectFailed: "接続に失敗しました: {message}",
driverNotInstalled: "{driver} ドライバーがインストールされていません。ドライバーマネージャーからインストールしてください。",
phoenixDriverNotInstalled: "Apache Phoenix JDBC ドライバーがインストールされていません。ドライバーマネージャーからインストールして、再試行してください。",
jreNotInstalled: "JRE {jre} ランタイムがインストールされていません。ドライバーマネージャーからインストールしてください。",
systemJavaNotFound: "システムJavaランタイムがPATHに見つかりませんでした。Javaをインストールするか、カスタムJava実行ファイルを選択してください。",
customJavaPathEmpty: "カスタムJavaランタイムのパスが空です。Java実行ファイルを選択してください。",
@@ -583,6 +584,14 @@ export default withEnglishFallback({
tunnelProfileManage: "管理",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "Legacy JDBC",
phoenixDirectMode: "直接接続",
phoenixRuntimeOption: "Apache Phoenix JDBC({mode})",
phoenixQueryServerMode: "Query Server",
phoenixDirectModeHint: "HBase に直接接続します。既定のランタイムは Phoenix 5.2.1 / HBase 2.5 向けです。その他のクラスターバージョンでは、互換性のある Phoenix クライアントを選択し、必要に応じて hbase-site.xml を追加してください。",
phoenixQueryServerModeHint: "Phoenix Thin Client を使用して、別途デプロイされた Phoenix Query Server(PQS)に HTTP 経由で接続します。",
phoenixDriverManagerHintPrefix: "既定のランタイムは",
phoenixDriverManagerHintSuffix: "でインストールまたはアンインストールできます。手動で選択した互換性のあるドライバーパスが優先されます。",
phoenixDocs: "Phoenix ドキュメントを表示",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "ドライバークラス(任意)",
+9
View File
@@ -581,6 +581,7 @@ export default withEnglishFallback({
connectSuccess: "{name}에 연결됨",
connectFailed: "연결 실패: {message}",
driverNotInstalled: "{driver} 드라이버가 설치되지 않았습니다. 드라이버 관리자에서 설치하세요.",
phoenixDriverNotInstalled: "Apache Phoenix JDBC 드라이버가 설치되지 않았습니다. 드라이버 관리자에서 설치한 후 다시 시도하세요.",
jreNotInstalled: "JRE {jre} 런타임이 설치되지 않았습니다. 드라이버 관리자에서 설치하세요.",
systemJavaNotFound: "PATH에서 시스템 Java 런타임을 찾을 수 없습니다. Java를 설치하거나 사용자 지정 Java 실행 파일을 선택하세요.",
customJavaPathEmpty: "사용자 지정 Java 런타임 경로가 비어 있습니다. Java 실행 파일을 선택하세요.",
@@ -715,6 +716,14 @@ export default withEnglishFallback({
httpTunnelInvalidTimeout: "{hop}: HTTP 터널 제한 시간은 1에서 300초 사이여야 합니다",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "레거시 JDBC",
phoenixDirectMode: "직접 연결",
phoenixRuntimeOption: "Apache Phoenix JDBC({mode})",
phoenixQueryServerMode: "Query Server",
phoenixDirectModeHint: "HBase에 직접 연결합니다. 기본 런타임은 Phoenix 5.2.1 및 HBase 2.5를 대상으로 합니다. 다른 클러스터 버전에서는 호환되는 Phoenix 클라이언트를 선택하고 필요한 경우 hbase-site.xml을 추가하세요.",
phoenixQueryServerModeHint: "Phoenix Thin Client를 사용하여 별도로 배포된 Phoenix Query Server(PQS)에 HTTP로 연결합니다.",
phoenixDriverManagerHintPrefix: "기본 런타임은 ",
phoenixDriverManagerHintSuffix: "에서 설치하거나 제거할 수 있습니다. 수동으로 선택한 호환 드라이버 경로가 우선합니다.",
phoenixDocs: "Phoenix 문서 보기",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "드라이버 클래스 (선택)",
+9
View File
@@ -485,6 +485,7 @@ export default withEnglishFallback({
connectSuccess: "Conectado a {name}",
connectFailed: "Falha na conexão: {message}",
driverNotInstalled: "O driver {driver} não está instalado. Instale-o pelo Gerenciador de Drivers.",
phoenixDriverNotInstalled: "O driver JDBC do Apache Phoenix não está instalado. Instale-o pelo Gerenciador de Drivers e tente novamente.",
jreNotInstalled: "O runtime JRE {jre} não está instalado. Instale-o pelo Gerenciador de Drivers.",
systemJavaNotFound: "O runtime Java do sistema não foi encontrado no PATH. Instale o Java ou escolha um executável Java personalizado.",
customJavaPathEmpty: "O caminho do runtime Java personalizado está vazio. Escolha um executável Java.",
@@ -590,6 +591,14 @@ export default withEnglishFallback({
tunnelProfileManage: "Gerenciar",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "JDBC legado",
phoenixDirectMode: "Direto",
phoenixRuntimeOption: "Apache Phoenix JDBC ({mode})",
phoenixQueryServerMode: "Query Server",
phoenixDirectModeHint: "Conecta diretamente ao HBase. O runtime padrão é destinado ao Phoenix 5.2.1 com HBase 2.5; para outras versões do cluster, selecione um cliente Phoenix compatível e adicione o hbase-site.xml quando necessário.",
phoenixQueryServerModeHint: "Conecta via HTTP a um Phoenix Query Server (PQS) implantado separadamente usando o cliente thin do Phoenix.",
phoenixDriverManagerHintPrefix: "Instale ou desinstale o runtime padrão no ",
phoenixDriverManagerHintSuffix: ". Caminhos de drivers compatíveis selecionados manualmente têm prioridade.",
phoenixDocs: "Ver documentação do Phoenix",
jdbcUrl: "URL JDBC",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "Classe do Driver (opcional)",
+9
View File
@@ -668,6 +668,7 @@ export default withEnglishFallback({
connectSuccess: "已连接 {name}",
connectFailed: "连接失败:{message}",
driverNotInstalled: "{driver} 驱动未安装,请在驱动管理器中安装。",
phoenixDriverNotInstalled: "Apache Phoenix JDBC 驱动未安装,请在驱动管理器中安装后重试。",
jreNotInstalled: "JRE {jre} 运行环境未安装,请在驱动管理器中安装。",
systemJavaNotFound: "未在 PATH 中找到系统 Java 运行环境,请安装 Java 或选择自定义 Java 可执行文件。",
customJavaPathEmpty: "自定义 Java 运行环境路径为空,请选择 Java 可执行文件。",
@@ -802,6 +803,14 @@ export default withEnglishFallback({
httpTunnelInvalidTimeout: "{hop}:HTTP 隧道超时时间必须在 1 到 300 秒之间",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "Legacy JDBC",
phoenixDirectMode: "直连",
phoenixRuntimeOption: "Apache Phoenix JDBC({mode})",
phoenixQueryServerMode: "查询服务器",
phoenixDirectModeHint: "直接连接 HBase。默认运行时适用于 Phoenix 5.2.1 与 HBase 2.5;其他集群版本可选择兼容的 Phoenix 客户端,并在需要时加入 hbase-site.xml。",
phoenixQueryServerModeHint: "通过 HTTP 连接到单独部署的 Phoenix Query Server(PQS),并使用 Phoenix Thin Client。",
phoenixDriverManagerHintPrefix: "请在",
phoenixDriverManagerHintSuffix: "中安装或卸载默认运行时;手动选择的兼容驱动路径优先。",
phoenixDocs: "查看 Phoenix 文档",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "驱动类(可选)",
+9
View File
@@ -484,6 +484,7 @@ export default withEnglishFallback({
connectSuccess: "已連線 {name}",
connectFailed: "連線失敗:{message}",
driverNotInstalled: "{driver} 驅動程式未安裝,請在驅動程式管理器中安裝。",
phoenixDriverNotInstalled: "Apache Phoenix JDBC 驅動程式未安裝,請在驅動程式管理器中安裝後再試。",
jreNotInstalled: "JRE {jre} 執行環境未安裝,請在驅動程式管理器中安裝。",
systemJavaNotFound: "未在 PATH 中找到系統 Java 執行環境,請安裝 Java 或選擇自訂 Java 可執行檔。",
customJavaPathEmpty: "自訂 Java 執行環境路徑為空,請選擇 Java 可執行檔。",
@@ -589,6 +590,14 @@ export default withEnglishFallback({
tunnelProfileManage: "管理",
dremioArrowFlightSqlMode: "Arrow Flight SQL",
dremioLegacyJdbcMode: "Legacy JDBC",
phoenixDirectMode: "直接連線",
phoenixRuntimeOption: "Apache Phoenix JDBC({mode})",
phoenixQueryServerMode: "查詢伺服器",
phoenixDirectModeHint: "直接連線 HBase。預設執行環境適用於 Phoenix 5.2.1 與 HBase 2.5;其他叢集版本可選取相容的 Phoenix 用戶端,並在需要時加入 hbase-site.xml。",
phoenixQueryServerModeHint: "透過 HTTP 連線至獨立部署的 Phoenix Query Server(PQS),並使用 Phoenix Thin Client。",
phoenixDriverManagerHintPrefix: "請在",
phoenixDriverManagerHintSuffix: "中安裝或解除安裝預設執行環境;手動選取的相容驅動程式路徑優先。",
phoenixDocs: "檢視 Phoenix 文件",
jdbcUrl: "JDBC URL",
jdbcUrlPlaceholder: "jdbc:postgresql://localhost:5432/database",
jdbcDriverClass: "驅動程式類別(可選)",
@@ -0,0 +1,163 @@
import { describe, expect, it, vi } from "vitest";
import {
createJdbcProductConnectionFieldsByMode,
createJdbcProductProfileRegistry,
ensureJdbcProductRuntimeDrivers,
isJdbcProductDefaultDriverClass,
jdbcProductManagedRuntimePaths,
rememberJdbcProductConnectionFields,
validateJdbcProductManagedDriver,
type JdbcProductProfileDefinition,
} from "@/lib/database/jdbcProductProfile";
import type { ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
import type { ConnectionConfig, JdbcMavenBundleInfo } from "@/types/database";
const directCoordinate = "com.example:fixture-direct:1.0.0";
const proxyCoordinate = "com.example:fixture-proxy:1.0.0";
const profile: JdbcProductProfileDefinition = {
id: "fixture",
label: "Fixture JDBC",
icon: "fixture",
category: "analytics",
port: 0,
user: "",
match: { dbType: "jdbc", driverProfile: "fixture" },
managedDriverId: "fixture",
runtimeLabelKey: "connection.fixtureRuntime",
driverManagerHintPrefixKey: "connection.fixtureHintPrefix",
driverManagerHintSuffixKey: "connection.fixtureHintSuffix",
docsLabelKey: "connection.fixtureDocs",
docsUrl: "https://example.com/docs",
missingDriverError: "Fixture driver is not installed.",
missingPluginError: "JDBC plugin is not installed.",
modes: [
{
id: "direct",
labelKey: "connection.fixtureDirect",
hintKey: "connection.fixtureDirectHint",
defaultConnectionString: "jdbc:fixture:direct",
defaultDriverClass: "com.example.DirectDriver",
managedCoordinates: [directCoordinate],
},
{
id: "proxy",
labelKey: "connection.fixtureProxy",
hintKey: "connection.fixtureProxyHint",
defaultConnectionString: "jdbc:fixture:proxy",
defaultDriverClass: "com.example.ProxyDriver",
managedCoordinates: [proxyCoordinate],
},
],
detectMode: (config) => (config.connection_string?.includes(":proxy") ? "proxy" : "direct"),
isCompatibleRuntimePath: (path, mode) => path.endsWith(`fixture-${mode}.jar`),
};
function config(mode: "direct" | "proxy" = "direct"): ConnectionConfig {
return {
id: "fixture-1",
name: "Fixture",
db_type: "jdbc",
driver_profile: "fixture",
host: "",
port: 0,
username: "",
password: "",
connection_string: `jdbc:fixture:${mode}`,
jdbc_driver_class: mode === "direct" ? "com.example.DirectDriver" : "com.example.ProxyDriver",
jdbc_driver_paths: [],
};
}
function bundle(coordinate: string): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
return {
id: coordinate.replaceAll(":", "_"),
coordinate,
scope: "runtime",
repositories: ["https://repo.example.com/"],
installed_at: "2026-08-06T00:00:00Z",
path: `/drivers/${artifactId}`,
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path: `/drivers/${artifactId}-${version}.jar`,
size: 1,
sha256: coordinate,
},
],
};
}
describe("jdbcProductProfile", () => {
it("rejects duplicate profile ids and matchers", () => {
expect(() => createJdbcProductProfileRegistry([{ ...profile, id: "" }])).toThrow(/id must not be empty/);
expect(() => createJdbcProductProfileRegistry([profile, { ...profile }])).toThrow(/Duplicate JDBC product profile id/);
expect(() => createJdbcProductProfileRegistry([profile, { ...profile, id: "fixture-copy" }])).toThrow(/Duplicate JDBC product profile matcher/);
});
it("requires every profile runtime coordinate to be owned by its managed driver", () => {
const driver: ManagedJdbcDriverDefinition = {
id: "fixture",
label: "Fixture JDBC",
version: "1.0.0",
jre: "21",
bundles: [
{ coordinate: directCoordinate, repositories: [] },
{ coordinate: proxyCoordinate, repositories: [] },
],
};
expect(() => validateJdbcProductManagedDriver(profile, driver)).not.toThrow();
expect(() => validateJdbcProductManagedDriver(profile, undefined)).toThrow(/unknown managed driver/);
expect(() => validateJdbcProductManagedDriver(profile, { ...driver, bundles: driver.bundles.slice(0, 1) })).toThrow(/requires coordinate not owned/);
});
it("provides reusable per-mode defaults and field memory", () => {
let fields = createJdbcProductConnectionFieldsByMode(profile);
fields = rememberJdbcProductConnectionFields(profile, fields, "direct", {
connectionString: "jdbc:fixture:custom",
driverClass: "com.example.CustomDriver",
});
expect(fields.direct).toEqual({ connectionString: "jdbc:fixture:custom", driverClass: "com.example.CustomDriver" });
expect(fields.proxy).toEqual({ connectionString: "jdbc:fixture:proxy", driverClass: "com.example.ProxyDriver" });
expect(isJdbcProductDefaultDriverClass(profile, "com.example.ProxyDriver")).toBe(true);
expect(isJdbcProductDefaultDriverClass(profile, "com.example.CustomDriver")).toBe(false);
});
it("selects the registered mode runtime while preserving unrelated paths", async () => {
const direct = bundle(directCoordinate);
const proxy = bundle(proxyCoordinate);
const connection = config("proxy");
connection.jdbc_driver_paths = [direct.artifacts[0].path, "/etc/fixture"];
const api = {
jdbcPluginStatus: vi.fn(async () => ({ installed: true, compatible: true })),
listJdbcMavenBundles: vi.fn(async () => [direct, proxy]),
};
const result = await ensureJdbcProductRuntimeDrivers(profile, connection, api);
expect(jdbcProductManagedRuntimePaths(profile, [direct, proxy], "proxy")).toEqual([proxy.artifacts[0].path]);
expect(result?.paths).toEqual(["/etc/fixture", proxy.artifacts[0].path]);
expect(result?.runtimeSelectionId).toBe("fixture:proxy");
});
it("preserves an explicitly selected compatible custom runtime", async () => {
const connection = config();
connection.jdbc_driver_paths = ["/opt/fixture-direct.jar", "/etc/fixture"];
const result = await ensureJdbcProductRuntimeDrivers(profile, connection, {
jdbcPluginStatus: vi.fn(async () => ({ installed: true, compatible: true })),
listJdbcMavenBundles: vi.fn(async () => []),
});
expect(result?.paths).toEqual(connection.jdbc_driver_paths);
expect(result?.runtimeSelectionId).toBeUndefined();
});
});
@@ -0,0 +1,125 @@
import { describe, expect, it, vi } from "vitest";
import {
createManagedJdbcDriverRegistry,
installManagedJdbcDriver,
managedJdbcDriverBundles,
managedJdbcDriverPaths,
managedJdbcDriverRow,
missingManagedJdbcDriverBundles,
uninstallManagedJdbcDriver,
type ManagedJdbcDriverApi,
type ManagedJdbcDriverDefinition,
} from "@/lib/database/managedJdbcDriver";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
const definition: ManagedJdbcDriverDefinition = {
id: "fixture",
label: "Fixture JDBC",
version: "1.0.0",
jre: "21",
bundles: [
{ coordinate: "com.example:fixture-core:1.0.0", repositories: ["https://repo.example.com/"] },
{ coordinate: "com.example:fixture-extra:1.0.0", repositories: ["https://repo.example.com/"] },
],
};
function bundle(coordinate: string, size = 10): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
return {
id: coordinate.replaceAll(":", "_"),
coordinate,
scope: "runtime",
repositories: ["https://repo.example.com/"],
installed_at: "2026-08-06T00:00:00Z",
path: `/drivers/${artifactId}`,
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path: `/drivers/${artifactId}-${version}.jar`,
size,
sha256: coordinate,
},
],
};
}
function pluginStatus(installed = true): JdbcPluginStatus {
return {
installed,
version: installed ? "1.0.0" : null,
protocol_version: installed ? 1 : null,
compatible: installed,
latest_version: null,
latest_protocol_version: null,
update_available: false,
path: "/plugins/jdbc",
};
}
function api(overrides: Partial<ManagedJdbcDriverApi> = {}): ManagedJdbcDriverApi {
return {
installJdbcPlugin: vi.fn(async () => pluginStatus()),
installJdbcDriverFromMaven: vi.fn(async () => [] as JdbcDriverInfo[]),
listJdbcMavenBundles: vi.fn(async () => []),
deleteJdbcMavenBundle: vi.fn(async () => [] as JdbcDriverInfo[]),
...overrides,
};
}
describe("managedJdbcDriver", () => {
it("rejects duplicate ids and ambiguous Maven bundle ownership", () => {
expect(() => createManagedJdbcDriverRegistry([{ ...definition, id: "" }])).toThrow(/id must not be empty/);
expect(() => createManagedJdbcDriverRegistry([definition, { ...definition }])).toThrow(/Duplicate managed JDBC driver id/);
expect(() => createManagedJdbcDriverRegistry([definition, { ...definition, id: "fixture-copy" }])).toThrow(/is owned by both/);
});
it("derives exact bundle state, paths, size, and plugin-aware installation", () => {
const core = bundle(definition.bundles[0].coordinate, 12);
const extra = bundle(definition.bundles[1].coordinate, 18);
const unrelated = bundle("com.example:unrelated:1.0.0", 999);
expect(managedJdbcDriverBundles(definition, [unrelated, extra, core])).toEqual([core, extra]);
expect(missingManagedJdbcDriverBundles(definition, [core])).toEqual([definition.bundles[1]]);
expect(managedJdbcDriverPaths(definition, [core, extra])).toEqual([core.artifacts[0].path, extra.artifacts[0].path]);
expect(managedJdbcDriverPaths(definition, [core])).toEqual([]);
expect(managedJdbcDriverRow(definition, [core, extra], pluginStatus())).toMatchObject({ installed: true, size: 30, installed_version: "1.0.0" });
expect(managedJdbcDriverRow(definition, [core, extra])).toMatchObject({ installed: false, size: 30, installed_version: null });
expect(managedJdbcDriverRow(definition, [core, extra], pluginStatus(false))).toMatchObject({ installed: false, size: 30, installed_version: null });
});
it("ensures the plugin and installs only missing coordinates", async () => {
const core = bundle(definition.bundles[0].coordinate);
const extra = bundle(definition.bundles[1].coordinate);
const installJdbcPlugin = vi.fn(async () => pluginStatus());
const installJdbcDriverFromMaven = vi.fn(async () => [] as JdbcDriverInfo[]);
const listJdbcMavenBundles = vi.fn(async () => [core, extra]);
const runtimeApi = api({ installJdbcPlugin, installJdbcDriverFromMaven, listJdbcMavenBundles });
const result = await installManagedJdbcDriver(definition, [core], pluginStatus(false), runtimeApi);
expect(installJdbcPlugin).toHaveBeenCalledOnce();
expect(installJdbcDriverFromMaven).toHaveBeenCalledOnce();
expect(installJdbcDriverFromMaven).toHaveBeenCalledWith(definition.bundles[1].coordinate, ["https://repo.example.com/"]);
expect(result.bundles).toEqual([core, extra]);
expect(result.pluginStatus?.installed).toBe(true);
});
it("uninstalls only bundles owned by the selected definition", async () => {
const core = bundle(definition.bundles[0].coordinate);
const extra = bundle(definition.bundles[1].coordinate);
const unrelated = bundle("com.example:unrelated:1.0.0");
const deleteJdbcMavenBundle = vi.fn(async () => [] as JdbcDriverInfo[]);
const listJdbcMavenBundles = vi.fn(async () => [unrelated]);
const runtimeApi = api({ deleteJdbcMavenBundle, listJdbcMavenBundles });
const result = await uninstallManagedJdbcDriver(definition, [unrelated, core, extra], runtimeApi);
expect(deleteJdbcMavenBundle.mock.calls).toEqual([[core.id], [extra.id]]);
expect(result.bundles).toEqual([unrelated]);
});
});
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from "vitest";
import { installRegisteredManagedJdbcDriver, managedJdbcDriverDefinition, managedJdbcDriverRows } from "@/lib/database/managedJdbcDrivers";
import { PHOENIX_MANAGED_MAVEN_COORDINATES } from "@/lib/database/phoenixBuiltinDriver";
import { PRESTOSQL_JDBC_DRIVER_COORDINATE } from "@/lib/database/prestoSqlBuiltinDriver";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
function pluginStatus(): JdbcPluginStatus {
return {
installed: true,
version: "1.0.0",
protocol_version: 1,
compatible: true,
latest_version: null,
latest_protocol_version: null,
update_available: false,
path: "/plugins/jdbc",
};
}
describe("managedJdbcDrivers", () => {
it("registers Phoenix and PrestoSQL through the same row model", () => {
expect(managedJdbcDriverDefinition("phoenix")?.bundles.map(({ coordinate }) => coordinate)).toEqual(PHOENIX_MANAGED_MAVEN_COORDINATES);
expect(managedJdbcDriverDefinition("prestosql")?.bundles.map(({ coordinate }) => coordinate)).toEqual([PRESTOSQL_JDBC_DRIVER_COORDINATE]);
expect(managedJdbcDriverRows([], pluginStatus()).map(({ db_type }) => db_type)).toEqual(["prestosql", "phoenix"]);
});
it("installs PrestoSQL through the generic Maven resolver", async () => {
const installed: JdbcMavenBundleInfo = {
id: "io.prestosql_presto-jdbc_350",
coordinate: PRESTOSQL_JDBC_DRIVER_COORDINATE,
scope: "runtime",
repositories: ["https://repo.maven.apache.org/maven2/"],
installed_at: "2026-08-06T00:00:00Z",
path: "/drivers/presto",
artifacts: [],
};
const installJdbcDriverFromMaven = vi.fn(async () => [] as JdbcDriverInfo[]);
const result = await installRegisteredManagedJdbcDriver("prestosql", [], pluginStatus(), {
installJdbcPlugin: vi.fn(async () => pluginStatus()),
installJdbcDriverFromMaven,
listJdbcMavenBundles: vi.fn(async () => [installed]),
deleteJdbcMavenBundle: vi.fn(async () => [] as JdbcDriverInfo[]),
});
expect(installJdbcDriverFromMaven).toHaveBeenCalledWith(PRESTOSQL_JDBC_DRIVER_COORDINATE, ["https://repo.maven.apache.org/maven2/"]);
expect(result?.bundles).toEqual([installed]);
});
});
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { PHOENIX_JDBC_DRIVER_VERSION, PHOENIX_MANAGED_JDBC_DRIVER, PHOENIX_MANAGED_MAVEN_COORDINATES } from "@/lib/database/phoenixBuiltinDriver";
import { PHOENIX_MAVEN_REPOSITORY } from "@/lib/database/phoenixConnection";
import { managedJdbcDriverBundles, managedJdbcDriverRow, missingManagedJdbcDriverBundles } from "@/lib/database/managedJdbcDriver";
import type { JdbcMavenBundleInfo } from "@/types/database";
function bundle(coordinate: string, size = 100): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
return {
id: `bundle:${coordinate}`,
coordinate,
scope: "runtime",
repositories: [PHOENIX_MAVEN_REPOSITORY],
installed_at: "2026-08-05T00:00:00Z",
path: "drivers/jdbc",
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path: `drivers/jdbc/${artifactId}-${version}.jar`,
size,
sha256: coordinate,
},
],
};
}
describe("phoenixBuiltinDriver", () => {
it("shows one uninstalled Apache Phoenix JDBC row when no managed bundle exists", () => {
const row = managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, []);
expect(row.db_type).toBe("phoenix");
expect(row.label).toBe("Apache Phoenix JDBC");
expect(row.version).toBe(PHOENIX_JDBC_DRIVER_VERSION);
expect(row.installed).toBe(false);
expect(row.installed_version).toBeNull();
expect(row.jre).toBe("21");
expect(missingManagedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, []).map(({ coordinate }) => coordinate)).toEqual(PHOENIX_MANAGED_MAVEN_COORDINATES);
});
it("stays uninstalled for a partial runtime and reports only missing coordinates", () => {
const direct = bundle(PHOENIX_MANAGED_MAVEN_COORDINATES[0], 120);
const row = managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, [direct]);
expect(row.installed).toBe(false);
expect(row.size).toBe(120);
expect(managedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [direct])).toEqual([direct]);
expect(missingManagedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [direct]).map(({ coordinate }) => coordinate)).toEqual(PHOENIX_MANAGED_MAVEN_COORDINATES.slice(1));
});
it("is installed only when Direct, logging, and PQS bundles are all present", () => {
const unrelated = bundle("com.mysql:mysql-connector-j:9.2.0", 999);
const managed = PHOENIX_MANAGED_MAVEN_COORDINATES.map((coordinate, index) => bundle(coordinate, index + 1));
const row = managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, [unrelated, ...managed], { installed: true, compatible: true });
expect(row.installed).toBe(true);
expect(row.installed_version).toBe(PHOENIX_JDBC_DRIVER_VERSION);
expect(row.size).toBe(6);
expect(managedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [unrelated, ...managed])).toEqual(managed);
expect(missingManagedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [unrelated, ...managed])).toEqual([]);
});
it("stays uninstalled when all bundles exist but the shared JDBC plugin is unavailable", () => {
const managed = PHOENIX_MANAGED_MAVEN_COORDINATES.map((coordinate) => bundle(coordinate));
expect(managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, managed, { installed: false, compatible: true }).installed).toBe(false);
expect(managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, managed, { installed: true, compatible: false }).installed).toBe(false);
});
});
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { PRESTOSQL_JDBC_DRIVER_COORDINATE, prestoSqlBuiltinDriverPaths, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
import { PRESTOSQL_JDBC_DRIVER_COORDINATE, PRESTOSQL_MANAGED_JDBC_DRIVER, prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver";
import { managedJdbcDriverBundles, managedJdbcDriverRow } from "@/lib/database/managedJdbcDriver";
import type { JdbcMavenBundleInfo } from "@/types/database";
function bundle(coordinate: string, version = "350"): JdbcMavenBundleInfo {
@@ -28,7 +29,7 @@ function bundle(coordinate: string, version = "350"): JdbcMavenBundleInfo {
describe("prestoSqlBuiltinDriver", () => {
it("adds an uninstalled PrestoSQL row when the Maven bundle is absent", () => {
const row = prestoSqlBuiltinDriverRow([]);
const row = managedJdbcDriverRow(PRESTOSQL_MANAGED_JDBC_DRIVER, []);
expect(row.db_type).toBe("prestosql");
expect(row.label).toBe("PrestoSQL");
@@ -40,9 +41,9 @@ describe("prestoSqlBuiltinDriver", () => {
it("marks PrestoSQL installed when its Maven coordinate is present", () => {
const installed = bundle(PRESTOSQL_JDBC_DRIVER_COORDINATE);
const row = prestoSqlBuiltinDriverRow([bundle("com.mysql:mysql-connector-j:9.2.0"), installed]);
const row = managedJdbcDriverRow(PRESTOSQL_MANAGED_JDBC_DRIVER, [bundle("com.mysql:mysql-connector-j:9.2.0"), installed], { installed: true, compatible: true });
expect(prestoSqlMavenBundle([installed])?.id).toBe(installed.id);
expect(managedJdbcDriverBundles(PRESTOSQL_MANAGED_JDBC_DRIVER, [installed])[0]?.id).toBe(installed.id);
expect(prestoSqlBuiltinDriverPaths([installed])).toEqual(["drivers/jdbc/presto-jdbc-350.jar"]);
expect(row.installed).toBe(true);
expect(row.installed_version).toBe("350");
@@ -70,7 +70,7 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => {
it("covers all known agent driver keys with no stale entries", () => {
// Golden set — mirrors the store-visible entries in agent_catalog.rs
// plus EXTRA_DRIVER_STORE_ENTRIES (duckdb, kafka, rocketmq,
// rabbitmq, sqlserver-legacy) and the PrestoSQL built-in driver.
// rabbitmq, sqlserver-legacy) and the built-in JDBC driver rows.
// When an Agent driver is added to the catalog its key MUST appear here;
// removing a key from the map without removing it from the list below
// will fail this test.
@@ -89,6 +89,7 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => {
"exasol",
"hive",
"kylin",
"phoenix",
"prestosql",
"saphana",
"snowflake",
@@ -145,8 +146,9 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => {
});
});
describe("prestosql matches ConnectionDialog category", () => {
it("is mapped to analytics (same as ConnectionDialog dbCategoryDefinitions)", () => {
describe("built-in JDBC drivers match ConnectionDialog categories", () => {
it("maps PrestoSQL and Apache Phoenix to analytics", () => {
expect(getCategoryForAgentDriver("prestosql")).toBe("analytics");
expect(getCategoryForAgentDriver("phoenix")).toBe("analytics");
});
});
@@ -47,6 +47,7 @@ export const AGENT_DRIVER_CATEGORY_MAP: Readonly<Record<string, DriverCategoryKe
"oceanbase-oracle": "domestic",
oracle: "sql",
oscar: "domestic",
phoenix: "analytics",
prestosql: "analytics",
rabbitmq: "mq",
rocketmq: "mq",
@@ -0,0 +1,232 @@
import type { ConnectionConfig, DatabaseType, JdbcMavenBundleInfo } from "@/types/database";
import type { ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
export type JdbcProductConnectionFields = {
connectionString: string;
driverClass: string;
};
export type JdbcProductConnectionFieldsByMode = Record<string, JdbcProductConnectionFields>;
export type JdbcProductModeDefinition = {
id: string;
labelKey: string;
hintKey: string;
defaultConnectionString: string;
defaultDriverClass: string;
managedCoordinates: readonly string[];
};
export type JdbcProductProfileDefinition = {
id: string;
label: string;
icon: string;
category: string;
port: number;
user: string;
match: {
dbType: DatabaseType;
driverProfile?: string;
};
managedDriverId: string;
runtimeLabelKey: string;
driverManagerHintPrefixKey: string;
driverManagerHintSuffixKey: string;
docsLabelKey: string;
docsUrl: string;
missingDriverError: string;
missingPluginError: string;
modes: readonly JdbcProductModeDefinition[];
detectMode: (config: Partial<Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">>) => string;
isCompatibleRuntimePath: (path: string, mode: string) => boolean;
};
export type JdbcProductRuntimeApi = {
jdbcPluginStatus: () => Promise<{ installed: boolean; compatible: boolean }>;
listJdbcMavenBundles: () => Promise<JdbcMavenBundleInfo[]>;
};
export type JdbcProductRuntimeResult = {
profile: JdbcProductProfileDefinition;
mode: JdbcProductModeDefinition;
bundles: JdbcMavenBundleInfo[];
paths: string[];
runtimeSelectionId?: string;
};
export type JdbcProductRuntimeErrorKind = "plugin-missing" | "driver-missing";
export class JdbcProductRuntimeError extends Error {
readonly kind: JdbcProductRuntimeErrorKind;
readonly profileId: string;
readonly managedDriverId: string;
constructor(kind: JdbcProductRuntimeErrorKind, profile: JdbcProductProfileDefinition) {
super(kind === "plugin-missing" ? profile.missingPluginError : profile.missingDriverError);
this.name = "JdbcProductRuntimeError";
this.kind = kind;
this.profileId = profile.id;
this.managedDriverId = profile.managedDriverId;
}
}
export type JdbcProductProfileRegistry = {
profiles: readonly JdbcProductProfileDefinition[];
byId: ReadonlyMap<string, JdbcProductProfileDefinition>;
};
export function createJdbcProductProfileRegistry(profiles: readonly JdbcProductProfileDefinition[]): JdbcProductProfileRegistry {
const byId = new Map<string, JdbcProductProfileDefinition>();
const matcherKeys = new Set<string>();
for (const profile of profiles) {
if (!profile.id.trim()) throw new Error("JDBC product profile id must not be empty");
if (byId.has(profile.id)) throw new Error(`Duplicate JDBC product profile id: ${profile.id}`);
const matcherKey = `${profile.match.dbType}:${profile.match.driverProfile ?? "*"}`;
if (matcherKeys.has(matcherKey)) throw new Error(`Duplicate JDBC product profile matcher: ${matcherKey}`);
if (profile.modes.length === 0) throw new Error(`JDBC product profile must define at least one mode: ${profile.id}`);
byId.set(profile.id, profile);
matcherKeys.add(matcherKey);
}
return { profiles: [...profiles], byId };
}
export function jdbcProductProfileMatches(profile: JdbcProductProfileDefinition, config: Pick<ConnectionConfig, "db_type" | "driver_profile">): boolean {
return config.db_type === profile.match.dbType && (profile.match.driverProfile === undefined || config.driver_profile === profile.match.driverProfile);
}
export function jdbcProductMode(profile: JdbcProductProfileDefinition, modeId: string): JdbcProductModeDefinition {
const mode = profile.modes.find((candidate) => candidate.id === modeId) ?? profile.modes[0];
if (!mode) throw new Error(`JDBC product profile has no modes: ${profile.id}`);
return mode;
}
export function jdbcProductConnectionDefaults(profile: JdbcProductProfileDefinition, modeId: string): JdbcProductConnectionFields {
const mode = jdbcProductMode(profile, modeId);
return {
connectionString: mode.defaultConnectionString,
driverClass: mode.defaultDriverClass,
};
}
export function createJdbcProductConnectionFieldsByMode(profile: JdbcProductProfileDefinition, config?: Partial<Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">>): JdbcProductConnectionFieldsByMode {
const fields = Object.fromEntries(profile.modes.map((mode) => [mode.id, jdbcProductConnectionDefaults(profile, mode.id)]));
if (!config) return fields;
const modeId = profile.detectMode(config);
const defaults = jdbcProductConnectionDefaults(profile, modeId);
fields[modeId] = {
connectionString: config.connection_string?.trim() || defaults.connectionString,
driverClass: config.jdbc_driver_class?.trim() || defaults.driverClass,
};
return fields;
}
export function rememberJdbcProductConnectionFields(profile: JdbcProductProfileDefinition, fields: JdbcProductConnectionFieldsByMode, modeId: string, current: Partial<JdbcProductConnectionFields>): JdbcProductConnectionFieldsByMode {
const defaults = jdbcProductConnectionDefaults(profile, modeId);
return {
...fields,
[modeId]: {
connectionString: current.connectionString?.trim() || defaults.connectionString,
driverClass: current.driverClass?.trim() || defaults.driverClass,
},
};
}
export function isJdbcProductDefaultDriverClass(profile: JdbcProductProfileDefinition, value: string | undefined): boolean {
const normalized = value?.trim() || "";
return !normalized || profile.modes.some((mode) => mode.defaultDriverClass === normalized);
}
export function jdbcProductManagedCoordinates(profile: JdbcProductProfileDefinition): string[] {
return Array.from(new Set(profile.modes.flatMap((mode) => [...mode.managedCoordinates])));
}
export function validateJdbcProductManagedDriver(profile: JdbcProductProfileDefinition, driver: ManagedJdbcDriverDefinition | undefined): void {
if (!driver || driver.id !== profile.managedDriverId) {
throw new Error(`JDBC product profile ${profile.id} references unknown managed driver: ${profile.managedDriverId}`);
}
const ownedCoordinates = new Set(driver.bundles.map(({ coordinate }) => coordinate));
const unownedCoordinate = jdbcProductManagedCoordinates(profile).find((coordinate) => !ownedCoordinates.has(coordinate));
if (unownedCoordinate) {
throw new Error(`JDBC product profile ${profile.id} requires coordinate not owned by ${driver.id}: ${unownedCoordinate}`);
}
}
export function isJdbcProductManagedMavenCoordinate(profile: JdbcProductProfileDefinition, coordinate: string): boolean {
return jdbcProductManagedCoordinates(profile).includes(coordinate);
}
export function jdbcProductManagedRuntimePaths(profile: JdbcProductProfileDefinition, bundles: readonly JdbcMavenBundleInfo[], modeId: string): string[] {
const bundleByCoordinate = new Map(bundles.map((bundle) => [bundle.coordinate, bundle]));
const requiredBundles = jdbcProductMode(profile, modeId).managedCoordinates.map((coordinate) => bundleByCoordinate.get(coordinate));
if (requiredBundles.some((bundle) => !bundle)) return [];
return requiredBundles.flatMap((bundle) => (bundle?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean));
}
export function jdbcProductRuntimeSelectionId(profile: JdbcProductProfileDefinition, modeId: string): string {
return `${profile.id}:${modeId}`;
}
function mavenBundleId(coordinate: string): string {
return coordinate
.trim()
.split("")
.map((character) => (/^[A-Za-z0-9.-]$/.test(character) ? character : "_"))
.join("");
}
export function isJdbcProductManagedMavenPath(profile: JdbcProductProfileDefinition, path: string): boolean {
const normalized = path.replaceAll("\\", "/");
return jdbcProductManagedCoordinates(profile).some((coordinate) => normalized.split("/").includes(mavenBundleId(coordinate)));
}
export function isJdbcProductRuntimeInstallError(profile: JdbcProductProfileDefinition, message: string): boolean {
return message.includes(profile.missingDriverError) || message.includes(profile.missingPluginError);
}
export async function ensureJdbcProductRuntimeDrivers(profile: JdbcProductProfileDefinition, config: ConnectionConfig, api: JdbcProductRuntimeApi): Promise<JdbcProductRuntimeResult | undefined> {
if (!jdbcProductProfileMatches(profile, config)) return undefined;
const modeId = profile.detectMode(config);
const mode = jdbcProductMode(profile, modeId);
const defaults = jdbcProductConnectionDefaults(profile, modeId);
config.connection_string = config.connection_string?.trim() || defaults.connectionString;
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || defaults.driverClass;
const configuredPaths = (config.jdbc_driver_paths ?? []).map((path) => path.trim()).filter(Boolean);
const pluginStatus = await api.jdbcPluginStatus();
if (!pluginStatus.installed || !pluginStatus.compatible) {
throw new JdbcProductRuntimeError("plugin-missing", profile);
}
const bundles = await api.listJdbcMavenBundles();
const managedCoordinates = jdbcProductManagedCoordinates(profile);
const managedBundles = bundles.filter((bundle) => managedCoordinates.includes(bundle.coordinate));
const managedPaths = new Set(managedBundles.flatMap((bundle) => bundle.artifacts.map((artifact) => artifact.path).filter(Boolean)));
const hasCustomRuntime = configuredPaths.some((path) => profile.isCompatibleRuntimePath(path, modeId) && !managedPaths.has(path) && !isJdbcProductManagedMavenPath(profile, path));
if (hasCustomRuntime) {
config.jdbc_driver_paths = configuredPaths;
return { profile, mode, bundles, paths: configuredPaths };
}
const installedCoordinates = new Set(bundles.map((bundle) => bundle.coordinate));
if (mode.managedCoordinates.some((coordinate) => !installedCoordinates.has(coordinate))) {
throw new JdbcProductRuntimeError("driver-missing", profile);
}
const customPaths = configuredPaths.filter((path) => !managedPaths.has(path) && !isJdbcProductManagedMavenPath(profile, path));
const runtimePaths = jdbcProductManagedRuntimePaths(profile, bundles, modeId);
if (runtimePaths.length === 0) {
throw new JdbcProductRuntimeError("driver-missing", profile);
}
const paths = Array.from(new Set([...customPaths, ...runtimePaths]));
config.jdbc_driver_paths = paths;
return {
profile,
mode,
bundles,
paths,
runtimeSelectionId: jdbcProductRuntimeSelectionId(profile, modeId),
};
}
@@ -0,0 +1,57 @@
import type { ConnectionConfig } from "@/types/database";
import { PHOENIX_JDBC_PRODUCT_PROFILE } from "@/lib/database/phoenixConnection";
import { createJdbcProductProfileRegistry, ensureJdbcProductRuntimeDrivers, isJdbcProductRuntimeInstallError, jdbcProductProfileMatches, validateJdbcProductManagedDriver, type JdbcProductProfileDefinition, type JdbcProductRuntimeApi } from "@/lib/database/jdbcProductProfile";
import { managedJdbcDriverDefinition } from "@/lib/database/managedJdbcDrivers";
export const JDBC_PRODUCT_PROFILES = [PHOENIX_JDBC_PRODUCT_PROFILE] as const satisfies readonly JdbcProductProfileDefinition[];
const jdbcProductProfileRegistry = createJdbcProductProfileRegistry(JDBC_PRODUCT_PROFILES);
for (const profile of JDBC_PRODUCT_PROFILES) {
validateJdbcProductManagedDriver(profile, managedJdbcDriverDefinition(profile.managedDriverId));
}
export function jdbcProductProfileDefinition(id: string): JdbcProductProfileDefinition | undefined {
return jdbcProductProfileRegistry.byId.get(id);
}
export function jdbcProductProfileForConfig(config: Pick<ConnectionConfig, "db_type" | "driver_profile">): JdbcProductProfileDefinition | undefined {
return JDBC_PRODUCT_PROFILES.find((profile) => jdbcProductProfileMatches(profile, config));
}
export function jdbcProductDriverProfiles() {
return Object.fromEntries(
JDBC_PRODUCT_PROFILES.map((profile) => [
profile.id,
{
type: profile.match.dbType,
port: profile.port,
user: profile.user,
label: profile.label,
icon: profile.icon,
},
]),
);
}
export function jdbcProductPickerOptions() {
return JDBC_PRODUCT_PROFILES.map((profile) => ({ value: profile.id, label: profile.label }));
}
export function jdbcProductIconTypes(): Record<string, string> {
return Object.fromEntries(JDBC_PRODUCT_PROFILES.map((profile) => [profile.id, profile.icon]));
}
export function jdbcProductProfileIdsForCategory(category: string): string[] {
return JDBC_PRODUCT_PROFILES.filter((profile) => profile.category === category).map((profile) => profile.id);
}
export function isRegisteredJdbcProductRuntimeInstallError(config: Pick<ConnectionConfig, "db_type" | "driver_profile">, message: string): boolean {
const profile = jdbcProductProfileForConfig(config);
return Boolean(profile && isJdbcProductRuntimeInstallError(profile, message));
}
export async function ensureRegisteredJdbcProductRuntimeDrivers(config: ConnectionConfig, api: JdbcProductRuntimeApi) {
const profile = jdbcProductProfileForConfig(config);
if (!profile) return undefined;
return ensureJdbcProductRuntimeDrivers(profile, config, api);
}
@@ -0,0 +1,110 @@
import type { AgentDriverInfo } from "@/lib/backend/api";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
export type ManagedJdbcBundleDefinition = {
coordinate: string;
repositories: readonly string[];
};
export type ManagedJdbcDriverDefinition = {
id: string;
label: string;
version: string;
jre: string;
bundles: readonly ManagedJdbcBundleDefinition[];
};
export type ManagedJdbcDriverApi = {
installJdbcPlugin: () => Promise<JdbcPluginStatus>;
installJdbcDriverFromMaven: (coordinate: string, repositories?: string[]) => Promise<JdbcDriverInfo[]>;
listJdbcMavenBundles: () => Promise<JdbcMavenBundleInfo[]>;
deleteJdbcMavenBundle: (bundleId: string) => Promise<JdbcDriverInfo[]>;
};
export type ManagedJdbcDriverMutationResult = {
drivers?: JdbcDriverInfo[];
bundles: JdbcMavenBundleInfo[];
pluginStatus?: JdbcPluginStatus;
};
export type ManagedJdbcDriverRegistry = {
definitions: readonly ManagedJdbcDriverDefinition[];
byId: ReadonlyMap<string, ManagedJdbcDriverDefinition>;
};
export function createManagedJdbcDriverRegistry(definitions: readonly ManagedJdbcDriverDefinition[]): ManagedJdbcDriverRegistry {
const byId = new Map<string, ManagedJdbcDriverDefinition>();
const coordinateOwners = new Map<string, string>();
for (const definition of definitions) {
if (!definition.id.trim()) throw new Error("Managed JDBC driver id must not be empty");
if (byId.has(definition.id)) throw new Error(`Duplicate managed JDBC driver id: ${definition.id}`);
if (definition.bundles.length === 0) throw new Error(`Managed JDBC driver must own at least one bundle: ${definition.id}`);
for (const bundle of definition.bundles) {
const owner = coordinateOwners.get(bundle.coordinate);
if (owner) throw new Error(`Managed JDBC coordinate ${bundle.coordinate} is owned by both ${owner} and ${definition.id}`);
coordinateOwners.set(bundle.coordinate, definition.id);
}
byId.set(definition.id, definition);
}
return { definitions: [...definitions], byId };
}
export function managedJdbcDriverBundles(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[]): JdbcMavenBundleInfo[] {
const bundleByCoordinate = new Map(bundles.map((bundle) => [bundle.coordinate, bundle]));
return definition.bundles.map(({ coordinate }) => bundleByCoordinate.get(coordinate)).filter((bundle): bundle is JdbcMavenBundleInfo => Boolean(bundle));
}
export function missingManagedJdbcDriverBundles(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[]): ManagedJdbcBundleDefinition[] {
const installedCoordinates = new Set(bundles.map((bundle) => bundle.coordinate));
return definition.bundles.filter(({ coordinate }) => !installedCoordinates.has(coordinate));
}
export function managedJdbcDriverPaths(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[], coordinates: readonly string[] = definition.bundles.map(({ coordinate }) => coordinate)): string[] {
const bundleByCoordinate = new Map(managedJdbcDriverBundles(definition, bundles).map((bundle) => [bundle.coordinate, bundle]));
const requiredBundles = coordinates.map((coordinate) => bundleByCoordinate.get(coordinate));
if (requiredBundles.some((bundle) => !bundle)) return [];
return requiredBundles.flatMap((bundle) => (bundle?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean));
}
export function managedJdbcDriverRow(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[], pluginStatus?: Pick<JdbcPluginStatus, "installed" | "compatible"> | null): AgentDriverInfo {
const installedBundles = managedJdbcDriverBundles(definition, bundles);
const pluginReady = pluginStatus?.installed === true && pluginStatus.compatible;
const installed = pluginReady && installedBundles.length === definition.bundles.length;
return {
db_type: definition.id,
label: definition.label,
version: definition.version,
size: installedBundles.reduce((bundleTotal, bundle) => bundleTotal + bundle.artifacts.reduce((artifactTotal, artifact) => artifactTotal + Number(artifact.size || 0), 0), 0),
installed,
installed_version: installed ? definition.version : null,
update_available: false,
requires_java_runtime: true,
jre: definition.jre,
jre_installed: true,
};
}
export async function installManagedJdbcDriver(definition: ManagedJdbcDriverDefinition, currentBundles: readonly JdbcMavenBundleInfo[], currentPluginStatus: Pick<JdbcPluginStatus, "installed" | "compatible"> | null | undefined, api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult> {
let pluginStatus: JdbcPluginStatus | undefined;
if (!currentPluginStatus?.installed || !currentPluginStatus.compatible) {
pluginStatus = await api.installJdbcPlugin();
}
let bundles = [...currentBundles];
let drivers: JdbcDriverInfo[] | undefined;
for (const bundle of definition.bundles) {
if (bundles.some(({ coordinate }) => coordinate === bundle.coordinate)) continue;
drivers = await api.installJdbcDriverFromMaven(bundle.coordinate, [...bundle.repositories]);
bundles = await api.listJdbcMavenBundles();
}
return { drivers, bundles, pluginStatus };
}
export async function uninstallManagedJdbcDriver(definition: ManagedJdbcDriverDefinition, currentBundles: readonly JdbcMavenBundleInfo[], api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult> {
let drivers: JdbcDriverInfo[] | undefined;
for (const bundle of managedJdbcDriverBundles(definition, currentBundles)) {
drivers = await api.deleteJdbcMavenBundle(bundle.id);
}
const bundles = await api.listJdbcMavenBundles();
return { drivers, bundles };
}
@@ -0,0 +1,32 @@
import type { JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
import { PHOENIX_MANAGED_JDBC_DRIVER } from "@/lib/database/phoenixBuiltinDriver";
import { PRESTOSQL_MANAGED_JDBC_DRIVER } from "@/lib/database/prestoSqlBuiltinDriver";
import { createManagedJdbcDriverRegistry, installManagedJdbcDriver, managedJdbcDriverRow, uninstallManagedJdbcDriver, type ManagedJdbcDriverApi, type ManagedJdbcDriverDefinition, type ManagedJdbcDriverMutationResult } from "@/lib/database/managedJdbcDriver";
export const MANAGED_JDBC_DRIVERS = [PRESTOSQL_MANAGED_JDBC_DRIVER, PHOENIX_MANAGED_JDBC_DRIVER] as const satisfies readonly ManagedJdbcDriverDefinition[];
const managedJdbcDriverRegistry = createManagedJdbcDriverRegistry(MANAGED_JDBC_DRIVERS);
export function managedJdbcDriverDefinition(id: string): ManagedJdbcDriverDefinition | undefined {
return managedJdbcDriverRegistry.byId.get(id);
}
export function isManagedJdbcDriver(id: string): boolean {
return managedJdbcDriverRegistry.byId.has(id);
}
export function managedJdbcDriverRows(bundles: readonly JdbcMavenBundleInfo[], pluginStatus?: Pick<JdbcPluginStatus, "installed" | "compatible"> | null) {
return MANAGED_JDBC_DRIVERS.map((definition) => managedJdbcDriverRow(definition, bundles, pluginStatus));
}
export async function installRegisteredManagedJdbcDriver(id: string, bundles: readonly JdbcMavenBundleInfo[], pluginStatus: Pick<JdbcPluginStatus, "installed" | "compatible"> | null | undefined, api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult | undefined> {
const definition = managedJdbcDriverDefinition(id);
if (!definition) return undefined;
return installManagedJdbcDriver(definition, bundles, pluginStatus, api);
}
export async function uninstallRegisteredManagedJdbcDriver(id: string, bundles: readonly JdbcMavenBundleInfo[], api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult | undefined> {
const definition = managedJdbcDriverDefinition(id);
if (!definition) return undefined;
return uninstallManagedJdbcDriver(definition, bundles, api);
}
@@ -0,0 +1,16 @@
import { PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE, PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DRIVER_PROFILE, PHOENIX_QUERY_SERVER_MAVEN_COORDINATE } from "@/lib/database/phoenixConnection";
import type { ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
export const PHOENIX_JDBC_DRIVER_VERSION = "5.2.1 / 6.0.0";
export const PHOENIX_MANAGED_MAVEN_COORDINATES = [PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE, PHOENIX_QUERY_SERVER_MAVEN_COORDINATE] as const;
export const PHOENIX_MANAGED_JDBC_DRIVER: ManagedJdbcDriverDefinition = {
id: PHOENIX_DRIVER_PROFILE,
label: "Apache Phoenix JDBC",
version: PHOENIX_JDBC_DRIVER_VERSION,
jre: "21",
bundles: PHOENIX_MANAGED_MAVEN_COORDINATES.map((coordinate) => ({
coordinate,
repositories: ["https://repo.maven.apache.org/maven2/"],
})),
};
@@ -0,0 +1,68 @@
import type { ConnectionConfig } from "@/types/database";
import type { JdbcProductProfileDefinition } from "@/lib/database/jdbcProductProfile";
export const PHOENIX_DRIVER_PROFILE = "phoenix";
export const PHOENIX_DIRECT_JDBC_URL = "jdbc:phoenix:localhost";
export const PHOENIX_DIRECT_JDBC_DRIVER_CLASS = "org.apache.phoenix.jdbc.PhoenixDriver";
export const PHOENIX_QUERY_SERVER_JDBC_URL = "jdbc:phoenix:thin:url=http://127.0.0.1:8765;serialization=PROTOBUF";
export const PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS = "org.apache.phoenix.queryserver.client.Driver";
export const PHOENIX_MAVEN_REPOSITORY = "https://repo.maven.apache.org/maven2/";
export const PHOENIX_DIRECT_MAVEN_COORDINATE = "org.apache.phoenix:phoenix-client-embedded-hbase-2.5:5.2.1";
export const PHOENIX_QUERY_SERVER_MAVEN_COORDINATE = "org.apache.phoenix:phoenix-queryserver-client:6.0.0";
export const PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE = "ch.qos.reload4j:reload4j:1.2.26";
export const PHOENIX_DRIVER_NOT_INSTALLED_ERROR = "Apache Phoenix JDBC driver is not installed. Install it from the Driver Manager, then retry.";
export const PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR = "DBX JDBC plugin is not installed. Install Apache Phoenix JDBC from the Driver Manager, then retry.";
export type PhoenixConnectionMode = "direct" | "query-server";
type PhoenixConnectionConfig = Partial<Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">>;
export const PHOENIX_JDBC_PRODUCT_PROFILE: JdbcProductProfileDefinition = {
id: PHOENIX_DRIVER_PROFILE,
label: "Apache Phoenix",
icon: "phoenix",
category: "analytics",
port: 0,
user: "",
match: { dbType: "jdbc", driverProfile: PHOENIX_DRIVER_PROFILE },
managedDriverId: PHOENIX_DRIVER_PROFILE,
runtimeLabelKey: "connection.phoenixRuntimeOption",
driverManagerHintPrefixKey: "connection.phoenixDriverManagerHintPrefix",
driverManagerHintSuffixKey: "connection.phoenixDriverManagerHintSuffix",
docsLabelKey: "connection.phoenixDocs",
docsUrl: "https://phoenix.apache.org/docs/fundamentals/client-classpath-and-jdbc-url/",
missingDriverError: PHOENIX_DRIVER_NOT_INSTALLED_ERROR,
missingPluginError: PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR,
modes: [
{
id: "direct",
labelKey: "connection.phoenixDirectMode",
hintKey: "connection.phoenixDirectModeHint",
defaultConnectionString: PHOENIX_DIRECT_JDBC_URL,
defaultDriverClass: PHOENIX_DIRECT_JDBC_DRIVER_CLASS,
managedCoordinates: [PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE],
},
{
id: "query-server",
labelKey: "connection.phoenixQueryServerMode",
hintKey: "connection.phoenixQueryServerModeHint",
defaultConnectionString: PHOENIX_QUERY_SERVER_JDBC_URL,
defaultDriverClass: PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS,
managedCoordinates: [PHOENIX_QUERY_SERVER_MAVEN_COORDINATE],
},
],
detectMode: (config) => phoenixConnectionModeForConfig(config),
isCompatibleRuntimePath: (path, mode) => isPhoenixRuntimePath(path, mode as PhoenixConnectionMode),
};
export function phoenixConnectionModeForConfig(config: PhoenixConnectionConfig): PhoenixConnectionMode {
const connectionString = config.connection_string?.trim().toLowerCase() || "";
const driverClass = config.jdbc_driver_class?.trim().toLowerCase() || "";
return connectionString.startsWith("jdbc:phoenix:thin:") || driverClass === PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS.toLowerCase() ? "query-server" : "direct";
}
export function isPhoenixRuntimePath(path: string, mode: PhoenixConnectionMode): boolean {
const parts = path.split(/[/\\]/);
const fileName = parts[parts.length - 1] || path;
return mode === "direct" ? /^phoenix-client-(?:embedded-)?hbase-.+\.jar$/i.test(fileName) : /^phoenix-queryserver-client-.+\.jar$/i.test(fileName);
}
@@ -1,31 +1,23 @@
import type { AgentDriverInfo } from "@/lib/backend/api";
import type { JdbcMavenBundleInfo } from "@/types/database";
import { managedJdbcDriverPaths, type ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
export const PRESTOSQL_DRIVER_DB_TYPE = "prestosql";
export const PRESTOSQL_JDBC_DRIVER_VERSION = "350";
export const PRESTOSQL_JDBC_DRIVER_COORDINATE = `io.prestosql:presto-jdbc:${PRESTOSQL_JDBC_DRIVER_VERSION}`;
export const PRESTOSQL_JDBC_DRIVER_REPOSITORY = "https://repo.maven.apache.org/maven2/";
export function prestoSqlMavenBundle(bundles: JdbcMavenBundleInfo[]): JdbcMavenBundleInfo | undefined {
return bundles.find((bundle) => bundle.coordinate === PRESTOSQL_JDBC_DRIVER_COORDINATE);
}
export const PRESTOSQL_MANAGED_JDBC_DRIVER: ManagedJdbcDriverDefinition = {
id: PRESTOSQL_DRIVER_DB_TYPE,
label: "PrestoSQL",
version: PRESTOSQL_JDBC_DRIVER_VERSION,
jre: "21",
bundles: [
{
coordinate: PRESTOSQL_JDBC_DRIVER_COORDINATE,
repositories: [PRESTOSQL_JDBC_DRIVER_REPOSITORY],
},
],
};
export function prestoSqlBuiltinDriverPaths(bundles: JdbcMavenBundleInfo[]): string[] {
return (prestoSqlMavenBundle(bundles)?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean);
}
export function prestoSqlBuiltinDriverRow(bundles: JdbcMavenBundleInfo[]): AgentDriverInfo {
const installedBundle = prestoSqlMavenBundle(bundles);
return {
db_type: PRESTOSQL_DRIVER_DB_TYPE,
label: "PrestoSQL",
version: PRESTOSQL_JDBC_DRIVER_VERSION,
size: installedBundle ? installedBundle.artifacts.reduce((total, artifact) => total + Number(artifact.size || 0), 0) : 0,
installed: Boolean(installedBundle),
installed_version: installedBundle ? PRESTOSQL_JDBC_DRIVER_VERSION : null,
update_available: false,
requires_java_runtime: true,
jre: "21",
jre_installed: true,
};
return managedJdbcDriverPaths(PRESTOSQL_MANAGED_JDBC_DRIVER, bundles);
}
@@ -1005,6 +1005,7 @@ export const useConnectionStore = defineStore("connection", () => {
spark: "Apache Spark",
db2: "DB2",
informix: "Informix",
phoenix: "Apache Phoenix",
neo4j: "Neo4j",
cassandra: "Cassandra",
bigquery: "BigQuery",
+16
View File
@@ -20,6 +20,22 @@ test("uses driver profile for connection option icon identity", () => {
assert.equal(connectionIconType(baseConnection), "tidb");
});
test("presents a JDBC-backed Phoenix connection with its product identity", () => {
const connection = {
...baseConnection,
db_type: "jdbc" as const,
driver_profile: "phoenix",
driver_label: "Apache Phoenix",
host: "",
port: 0,
database: undefined,
};
assert.equal(connectionIconType(connection), "phoenix");
assert.equal(connectionDriverLabel(connection), "Apache Phoenix");
assert.equal(connectionOptionSubtitle(connection), "Apache Phoenix");
});
test("displays the configured GaussDB protocol in connection URLs", () => {
assert.equal(connectionDisplayUrlScheme({ db_type: "gaussdb", driver_profile: "gaussdb" }), "postgresql");
assert.equal(connectionDisplayUrlScheme({ db_type: "gaussdb", driver_profile: "gaussdb-m" }), "jdbc:gaussdb");
+15 -5
View File
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { test } from "vitest";
import { assertCompleteDatabaseCategories, databaseSelectionForCategory } from "../../apps/desktop/src/lib/connection/databaseCategoryOptions.ts";
import { JDBC_PRODUCT_PROFILES } from "../../apps/desktop/src/lib/database/jdbcProductProfiles.ts";
test("database categories cover every option exactly once", () => {
assert.doesNotThrow(() => assertCompleteDatabaseCategories(["mysql", "redis", "kafka"], [["mysql"], ["redis", "kafka"]]));
@@ -27,13 +28,22 @@ test("ConnectionDialog database categories stay exhaustive", () => {
assert.ok(categoriesMatch, "dbCategoryDefinitions not found");
const optionValues = [...optionsMatch[1].matchAll(/value:\s*"([^"]+)"/g)].map((match) => match[1]);
const categoryBlocks = [...categoriesMatch[1].matchAll(/optionValues:\s*\[([^\]]*)\]/g)].map((match) =>
[...match[1].matchAll(/"([^"]+)"/g)].map((valueMatch) => valueMatch[1]),
);
const categoryBlocks = [...categoriesMatch[1].matchAll(/optionValues:\s*\[([^\]]*)\]/g)].map((match) => [...match[1].matchAll(/"([^"]+)"/g)].map((valueMatch) => valueMatch[1]));
optionValues.push(...JDBC_PRODUCT_PROFILES.map((profile) => profile.id));
categoryBlocks.push(JDBC_PRODUCT_PROFILES.map((profile) => profile.id));
assert.doesNotThrow(() => assertCompleteDatabaseCategories(optionValues, categoryBlocks));
assert.match(source, /for \(const category of dbCategoryDefinitions\)/);
assert.match(source, /jdbcProductProfileIdsForCategory\(category\.key\)/);
assert.match(source, /\.\.\.jdbcProductIconTypes\(\)/);
assert.ok(optionValues.includes("rabbitmq"), "rabbitmq must remain in dbOptions");
assert.ok(categoryBlocks.some((values) => values.includes("rabbitmq")), "rabbitmq must remain categorized");
assert.ok(
categoryBlocks.some((values) => values.includes("rabbitmq")),
"rabbitmq must remain categorized",
);
assert.ok(optionValues.includes("uxdb"), "uxdb must remain in dbOptions");
assert.ok(categoryBlocks.some((values) => values.includes("uxdb")), "uxdb must remain categorized");
assert.ok(
categoryBlocks.some((values) => values.includes("uxdb")),
"uxdb must remain categorized",
);
});
+8
View File
@@ -14,3 +14,11 @@ test("RocketMQ database icon asset exists", () => {
const svg = readFileSync(path.resolve("apps/desktop/public/icons/database/rocketmq.svg"), "utf8");
assert.match(svg, /<svg/i);
});
test("Apache Phoenix database icon keeps its official source attribution", () => {
const svg = readFileSync(path.resolve("apps/desktop/public/icons/database/phoenix.svg"), "utf8");
assert.match(svg, /https:\/\/phoenix\.apache\.org\/images\/logo\.svg/);
assert.match(svg, /Apache Phoenix logo/);
assert.match(svg, /#f7931d/i);
assert.match(svg, /#ed1c24/i);
});
+2 -1
View File
@@ -28,7 +28,8 @@ test("opens driver management on the JDBC tab from JDBC connection settings", ()
const app = source("apps/desktop/src/App.vue");
const driverStore = source("apps/desktop/src/components/config/DriverStoreDialog.vue");
assert.equal(connectionDialog.match(/emit\('openDriverStore', \{ target: 'tab', tab: 'jdbc' \}\)/g)?.length, 2);
assert.equal(connectionDialog.match(/emit\('openDriverStore', \{ target: 'tab', tab: 'jdbc' \}\)/g)?.length, 1);
assert.match(connectionDialog, /isJdbcProductConnection\.value \? agentDriverFocus\.value : \{ target: "tab", tab: "jdbc" \}/);
assert.match(appDialogs, /@open-driver-store="emit\('openDriverStore', \$event\)"/);
assert.match(app, /openDriverStorePage\(\$event\)/);
assert.match(app, /v-model:active-tab="driverStoreActiveTab"/);
+14
View File
@@ -120,6 +120,20 @@ test("discovers schemas only for unknown generic JDBC connections", () => {
assert.equal(connectionShouldDiscoverJdbcSchemas({ db_type: "jdbc", connection_string: "jdbc:mysql://127.0.0.1:3306/app" }), false);
});
test("keeps Phoenix on the generic JDBC metadata and object tree path", () => {
const connection = {
db_type: "jdbc" as const,
driver_profile: "phoenix",
connection_string: "jdbc:phoenix:localhost",
jdbc_driver_class: "org.apache.phoenix.jdbc.PhoenixDriver",
};
assert.equal(inferJdbcDialect(connection), undefined);
assert.equal(effectiveDatabaseTypeForConnection(connection), "jdbc");
assert.equal(connectionShouldDiscoverJdbcSchemas(connection), true);
assert.equal(connectionUsesDatabaseObjectTreeMode(connection), true);
});
test("uses SQL Server editor syntax for ASE without changing its effective JDBC type", () => {
const aseConnections = [
{ db_type: "jdbc" as const, driver_profile: "ase" },
+258
View File
@@ -0,0 +1,258 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test, vi } from "vitest";
import {
isPhoenixRuntimePath,
PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE,
PHOENIX_DIRECT_MAVEN_COORDINATE,
PHOENIX_DIRECT_JDBC_DRIVER_CLASS,
PHOENIX_DIRECT_JDBC_URL,
PHOENIX_DRIVER_NOT_INSTALLED_ERROR,
PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR,
PHOENIX_JDBC_PRODUCT_PROFILE,
PHOENIX_MAVEN_REPOSITORY,
phoenixConnectionModeForConfig,
PHOENIX_QUERY_SERVER_MAVEN_COORDINATE,
PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS,
PHOENIX_QUERY_SERVER_JDBC_URL,
} from "../../apps/desktop/src/lib/database/phoenixConnection.ts";
import {
createJdbcProductConnectionFieldsByMode,
ensureJdbcProductRuntimeDrivers,
isJdbcProductDefaultDriverClass,
isJdbcProductManagedMavenCoordinate,
isJdbcProductManagedMavenPath,
isJdbcProductRuntimeInstallError,
jdbcProductConnectionDefaults,
jdbcProductManagedRuntimePaths,
jdbcProductMode,
jdbcProductRuntimeSelectionId,
rememberJdbcProductConnectionFields,
} from "../../apps/desktop/src/lib/database/jdbcProductProfile.ts";
import type { ConnectionConfig, JdbcMavenBundleInfo } from "../../apps/desktop/src/types/database.ts";
function phoenixConfig(mode: "direct" | "query-server" = "direct"): ConnectionConfig {
const defaults = jdbcProductConnectionDefaults(PHOENIX_JDBC_PRODUCT_PROFILE, mode);
return {
id: "phoenix-1",
name: "Apache Phoenix",
db_type: "jdbc",
driver_profile: "phoenix",
driver_label: "Apache Phoenix",
host: "",
port: 0,
username: "",
password: "",
connection_string: defaults.connectionString,
jdbc_driver_class: defaults.driverClass,
jdbc_driver_paths: [],
};
}
function mavenBundle(coordinate: string): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
const path = `/drivers/${artifactId}-${version}.jar`;
return {
id: `${artifactId}-${version}`,
coordinate,
scope: "runtime",
repositories: [PHOENIX_MAVEN_REPOSITORY],
installed_at: "2026-08-05T00:00:00Z",
path: `/drivers/${artifactId}-${version}`,
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path,
size: 1,
sha256: coordinate,
},
],
};
}
function runtimeApi(initialBundles: JdbcMavenBundleInfo[] = [], pluginInstalled = true) {
const bundles = [...initialBundles];
const jdbcPluginStatus = vi.fn(async () => ({ installed: pluginInstalled, compatible: pluginInstalled }));
const listJdbcMavenBundles = vi.fn(async () => [...bundles]);
return {
bundles,
api: {
jdbcPluginStatus,
listJdbcMavenBundles,
},
jdbcPluginStatus,
listJdbcMavenBundles,
};
}
test("provides official Phoenix JDBC defaults for both connection modes", () => {
assert.equal(PHOENIX_QUERY_SERVER_JDBC_URL, "jdbc:phoenix:thin:url=http://127.0.0.1:8765;serialization=PROTOBUF");
assert.deepEqual(jdbcProductConnectionDefaults(PHOENIX_JDBC_PRODUCT_PROFILE, "direct"), {
connectionString: PHOENIX_DIRECT_JDBC_URL,
driverClass: PHOENIX_DIRECT_JDBC_DRIVER_CLASS,
});
assert.deepEqual(jdbcProductConnectionDefaults(PHOENIX_JDBC_PRODUCT_PROFILE, "query-server"), {
connectionString: PHOENIX_QUERY_SERVER_JDBC_URL,
driverClass: PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS,
});
});
test("detects Query Server from either the thin URL or driver class", () => {
assert.equal(phoenixConnectionModeForConfig({ connection_string: PHOENIX_DIRECT_JDBC_URL }), "direct");
assert.equal(phoenixConnectionModeForConfig({ jdbc_driver_class: PHOENIX_DIRECT_JDBC_DRIVER_CLASS }), "direct");
assert.equal(phoenixConnectionModeForConfig({ connection_string: "jdbc:phoenix:thin:url=https://phoenix.example.com:8765" }), "query-server");
assert.equal(phoenixConnectionModeForConfig({ jdbc_driver_class: PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS }), "query-server");
});
test("hydrates only the detected mode and preserves custom Phoenix fields", () => {
const fields = createJdbcProductConnectionFieldsByMode(PHOENIX_JDBC_PRODUCT_PROFILE, {
connection_string: "jdbc:phoenix:thin:url=https://phoenix.example.com:8765;serialization=PROTOBUF",
jdbc_driver_class: "com.example.CustomPhoenixThinDriver",
});
assert.deepEqual(fields.direct, jdbcProductConnectionDefaults(PHOENIX_JDBC_PRODUCT_PROFILE, "direct"));
assert.deepEqual(fields["query-server"], {
connectionString: "jdbc:phoenix:thin:url=https://phoenix.example.com:8765;serialization=PROTOBUF",
driverClass: "com.example.CustomPhoenixThinDriver",
});
});
test("remembers independent custom values while switching modes", () => {
let fields = createJdbcProductConnectionFieldsByMode(PHOENIX_JDBC_PRODUCT_PROFILE);
fields = rememberJdbcProductConnectionFields(PHOENIX_JDBC_PRODUCT_PROFILE, fields, "direct", {
connectionString: "jdbc:phoenix:zk1,zk2:2181:/hbase-secure",
driverClass: "com.example.CustomPhoenixDriver",
});
fields = rememberJdbcProductConnectionFields(PHOENIX_JDBC_PRODUCT_PROFILE, fields, "query-server", {
connectionString: "jdbc:phoenix:thin:url=https://query.example.com:8765",
driverClass: PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS,
});
assert.deepEqual(fields.direct, {
connectionString: "jdbc:phoenix:zk1,zk2:2181:/hbase-secure",
driverClass: "com.example.CustomPhoenixDriver",
});
assert.deepEqual(fields["query-server"], {
connectionString: "jdbc:phoenix:thin:url=https://query.example.com:8765",
driverClass: PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS,
});
});
test("recognizes only generated Phoenix driver classes as replaceable defaults", () => {
assert.equal(isJdbcProductDefaultDriverClass(PHOENIX_JDBC_PRODUCT_PROFILE, undefined), true);
assert.equal(isJdbcProductDefaultDriverClass(PHOENIX_JDBC_PRODUCT_PROFILE, PHOENIX_DIRECT_JDBC_DRIVER_CLASS), true);
assert.equal(isJdbcProductDefaultDriverClass(PHOENIX_JDBC_PRODUCT_PROFILE, PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS), true);
assert.equal(isJdbcProductDefaultDriverClass(PHOENIX_JDBC_PRODUCT_PROFILE, "com.example.CustomPhoenixDriver"), false);
});
test("defines mode-specific Phoenix Maven runtimes", () => {
assert.deepEqual(jdbcProductMode(PHOENIX_JDBC_PRODUCT_PROFILE, "direct").managedCoordinates, [PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE]);
assert.deepEqual(jdbcProductMode(PHOENIX_JDBC_PRODUCT_PROFILE, "query-server").managedCoordinates, [PHOENIX_QUERY_SERVER_MAVEN_COORDINATE]);
assert.equal(isPhoenixRuntimePath("/drivers/phoenix-client-embedded-hbase-2.5-5.2.1.jar", "direct"), true);
assert.equal(isPhoenixRuntimePath("/drivers/phoenix-queryserver-client-6.0.0.jar", "query-server"), true);
assert.equal(isPhoenixRuntimePath("/drivers/phoenix-queryserver-client-6.0.0.jar", "direct"), false);
assert.equal(isJdbcProductManagedMavenPath(PHOENIX_JDBC_PRODUCT_PROFILE, "/plugins/jdbc/drivers/maven/org.apache.phoenix_phoenix-queryserver-client_6.0.0/jars/phoenix-queryserver-client-6.0.0.jar"), true);
assert.equal(isJdbcProductManagedMavenPath(PHOENIX_JDBC_PRODUCT_PROFILE, "/opt/phoenix-queryserver-client-6.0.0.jar"), false);
assert.equal(isJdbcProductManagedMavenCoordinate(PHOENIX_JDBC_PRODUCT_PROFILE, PHOENIX_DIRECT_MAVEN_COORDINATE), true);
assert.equal(isJdbcProductManagedMavenCoordinate(PHOENIX_JDBC_PRODUCT_PROFILE, "com.example:custom-driver:1.0.0"), false);
assert.equal(jdbcProductRuntimeSelectionId(PHOENIX_JDBC_PRODUCT_PROFILE, "direct"), "phoenix:direct");
});
test("builds one complete selector runtime for each installed Phoenix mode", () => {
const direct = mavenBundle(PHOENIX_DIRECT_MAVEN_COORDINATE);
const logging = mavenBundle(PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE);
const queryServer = mavenBundle(PHOENIX_QUERY_SERVER_MAVEN_COORDINATE);
assert.deepEqual(jdbcProductManagedRuntimePaths(PHOENIX_JDBC_PRODUCT_PROFILE, [direct, logging, queryServer], "direct"), [direct.artifacts[0].path, logging.artifacts[0].path]);
assert.deepEqual(jdbcProductManagedRuntimePaths(PHOENIX_JDBC_PRODUCT_PROFILE, [direct, logging, queryServer], "query-server"), [queryServer.artifacts[0].path]);
assert.deepEqual(jdbcProductManagedRuntimePaths(PHOENIX_JDBC_PRODUCT_PROFILE, [direct], "direct"), []);
});
test("requires the JDBC plugin to be installed from Driver Manager", async () => {
const config = phoenixConfig();
const runtime = runtimeApi([], false);
await assert.rejects(ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api), new RegExp(PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.equal(runtime.listJdbcMavenBundles.mock.calls.length, 0);
assert.equal(isJdbcProductRuntimeInstallError(PHOENIX_JDBC_PRODUCT_PROFILE, PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR), true);
});
test("does not download a missing Direct runtime while testing or connecting", async () => {
const config = phoenixConfig();
const runtime = runtimeApi();
await assert.rejects(ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api), new RegExp(PHOENIX_DRIVER_NOT_INSTALLED_ERROR.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.equal(runtime.listJdbcMavenBundles.mock.calls.length, 1);
assert.equal(isJdbcProductRuntimeInstallError(PHOENIX_JDBC_PRODUCT_PROFILE, PHOENIX_DRIVER_NOT_INSTALLED_ERROR), true);
});
test("reuses installed Direct bundles, removes stale managed PQS paths, and preserves configuration paths", async () => {
const direct = mavenBundle(PHOENIX_DIRECT_MAVEN_COORDINATE);
const logging = mavenBundle(PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE);
const queryServer = mavenBundle(PHOENIX_QUERY_SERVER_MAVEN_COORDINATE);
const config = phoenixConfig();
config.jdbc_driver_paths = [queryServer.artifacts[0].path, "/etc/hbase", "/opt/custom-auth.jar"];
const runtime = runtimeApi([direct, logging, queryServer]);
const result = await ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api);
assert.deepEqual(result?.paths, ["/etc/hbase", "/opt/custom-auth.jar", direct.artifacts[0].path, logging.artifacts[0].path]);
assert.equal(result?.runtimeSelectionId, "phoenix:direct");
});
test("does not treat a deleted DBX-managed bundle path as a custom runtime", async () => {
const config = phoenixConfig();
config.jdbc_driver_paths = ["/plugins/jdbc/drivers/maven/org.apache.phoenix_phoenix-client-embedded-hbase-2.5_5.2.1/jars/phoenix-client-embedded-hbase-2.5-5.2.1.jar"];
const runtime = runtimeApi();
await assert.rejects(ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api), new RegExp(PHOENIX_DRIVER_NOT_INSTALLED_ERROR.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});
test("keeps a manually selected Phoenix runtime untouched", async () => {
const config = phoenixConfig();
config.jdbc_driver_paths = ["/opt/phoenix-client-embedded-hbase-2.6-5.3.2.jar", "/etc/hbase", "/opt/reload4j-1.2.26.jar"];
const runtime = runtimeApi();
const result = await ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api);
assert.deepEqual(result?.paths, config.jdbc_driver_paths);
assert.equal(result?.runtimeSelectionId, undefined);
});
test("selects only the standalone PQS client for Query Server mode", async () => {
const config = phoenixConfig("query-server");
const queryServer = mavenBundle(PHOENIX_QUERY_SERVER_MAVEN_COORDINATE);
const runtime = runtimeApi([queryServer]);
const result = await ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api);
assert.deepEqual(result?.paths, ["/drivers/phoenix-queryserver-client-6.0.0.jar"]);
assert.equal(result?.runtimeSelectionId, "phoenix:query-server");
});
test("does not provision Phoenix runtimes for another JDBC profile", async () => {
const config = phoenixConfig();
config.driver_profile = "jdbc";
const runtime = runtimeApi();
assert.equal(await ensureJdbcProductRuntimeDrivers(PHOENIX_JDBC_PRODUCT_PROFILE, config, runtime.api), undefined);
assert.equal(runtime.jdbcPluginStatus.mock.calls.length, 0);
assert.equal(runtime.listJdbcMavenBundles.mock.calls.length, 0);
});
test("registers Apache Phoenix as a declarative JDBC picker profile with both modes", () => {
const profileSource = readFileSync(path.resolve("apps/desktop/src/lib/database/phoenixConnection.ts"), "utf8");
const registrySource = readFileSync(path.resolve("apps/desktop/src/lib/database/jdbcProductProfiles.ts"), "utf8");
assert.match(profileSource, /id:\s*PHOENIX_DRIVER_PROFILE/);
assert.match(profileSource, /match:\s*\{\s*dbType:\s*"jdbc",\s*driverProfile:\s*PHOENIX_DRIVER_PROFILE\s*\}/);
assert.match(profileSource, /id:\s*"direct"/);
assert.match(profileSource, /id:\s*"query-server"/);
assert.match(registrySource, /JDBC_PRODUCT_PROFILES\s*=\s*\[PHOENIX_JDBC_PRODUCT_PROFILE\]/);
});
+50
View File
@@ -0,0 +1,50 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import { test } from "vitest";
function source(relativePath: string): string {
return readFileSync(path.resolve(relativePath), "utf8");
}
test("renders registered managed JDBC rows in Driver Manager", () => {
const driverStore = source("apps/desktop/src/components/config/DriverStoreDialog.vue");
assert.match(driverStore, /managedJdbcDriverRows\(jdbcMavenBundles\.value, jdbcPluginStatus\.value\)/);
assert.match(driverStore, /isManagedJdbcBuiltinDriver\(dbType: string\)/);
assert.match(driverStore, /isManagedJdbcDriver\(dbType\)/);
});
test("dispatches install and uninstall through the shared managed JDBC lifecycle", () => {
const driverStore = source("apps/desktop/src/components/config/DriverStoreDialog.vue");
assert.match(driverStore, /installRegisteredManagedJdbcDriver\(dbType, jdbcMavenBundles\.value, jdbcPluginStatus\.value, api\)/);
assert.match(driverStore, /uninstallRegisteredManagedJdbcDriver\(dbType, jdbcMavenBundles\.value, api\)/);
});
test("routes managed product runtime failures back to the registered Driver Manager row", () => {
const connectionDialog = source("apps/desktop/src/components/connection/ConnectionDialog.vue");
assert.match(connectionDialog, /connectionErrorRawDetail\.value = message/);
assert.match(connectionDialog, /isRegisteredJdbcProductRuntimeInstallError\(form\.value, connectionErrorRawDetail\.value\)/);
assert.match(connectionDialog, /isJdbcProductConnection\.value \? agentDriverFocus\.value : \{ target: "tab", tab: "jdbc" \}/);
assert.match(connectionDialog, /emit\('openDriverStore', agentDriverFocus\)/);
});
test("treats each registered product mode as one complete runtime in the connection selector", () => {
const connectionDialog = source("apps/desktop/src/components/connection/ConnectionDialog.vue");
assert.match(connectionDialog, /jdbcProductManagedRuntimePaths\(productProfile, jdbcMavenBundles\.value, productMode\.id\)/);
assert.match(connectionDialog, /label: t\(productProfile\.runtimeLabelKey/);
assert.match(connectionDialog, /!isJdbcProductManagedMavenPath\(activeJdbcProductProfile\.value!, path\)/);
assert.doesNotMatch(connectionDialog, /async function loadJdbcDrivers\(\) \{\s*if \(!isDesktop\) return;/);
});
test("keeps product-specific branches out of generic integration files", () => {
for (const file of ["apps/desktop/src/components/connection/ConnectionDialog.vue", "apps/desktop/src/components/config/DriverStoreDialog.vue", "apps/desktop/src/App.vue"]) {
const contents = source(file);
assert.doesNotMatch(contents, /from ["']@\/lib\/database\/phoenix/);
assert.doesNotMatch(contents, /PHOENIX_DRIVER_PROFILE/);
assert.doesNotMatch(contents, /isPhoenix|ensurePhoenix|phoenixConnectionMode/);
}
});