mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat: add PrestoSQL connector
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 42.88 63.19"><defs><style>.cls-1{fill:#cdcccc;}.cls-2{fill:#fff;}.cls-3{fill:#dd00a1;}.cls-4{fill:#f9d8d2;}.cls-5{fill:#10110e;}.cls-6{fill:#e5e5e5;}.cls-7{fill:#8accce;opacity:0.2;}.cls-8{fill:#515151;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_3" data-name="Layer 3"><path class="cls-1" d="M39.09,30.67a3,3,0,0,0-6,0A2.76,2.76,0,0,0,35,33.22v4.57a15.27,15.27,0,0,0-6.91-7.85c.46-1,.93-1.86,1.38-2.65a65.83,65.83,0,0,1,5.67-7.24c1.13-1.34,2.2-2.6,3.07-3.7,2.44-3.06,5.12-7.58,4.62-10.26a2.56,2.56,0,0,0-1.25-1.81c-3.39-2-8,4.5-12.53,10.77-3,4.14-4.55,10-5.26,13.29a19.86,19.86,0,0,0-4.23-.46,18.63,18.63,0,0,0-6.08,1,85.14,85.14,0,0,1,.33-11c1-9.51.56-15.09-1.29-17A2.43,2.43,0,0,0,10.56,0C5.1.27,5.72,16.58,5.81,18.44c.22,4.46,2,9.88,3.08,12.88a15.44,15.44,0,0,0-5.25,7.81H3.25a3.11,3.11,0,0,0-3.25,3v5.17a3.12,3.12,0,0,0,3.25,3h.66A13.34,13.34,0,0,0,7.2,54.93l-1,.88a1.17,1.17,0,0,0-.13,1.64c3,3.65,7.91,5.74,13.42,5.74s10.44-2.09,13.42-5.74a1.17,1.17,0,0,0-.13-1.64l-1-.88a13.56,13.56,0,0,0,3.29-4.71h.75a3.12,3.12,0,0,0,3.25-3V42.09a2.94,2.94,0,0,0-1.91-2.68V33.22A2.75,2.75,0,0,0,39.09,30.67Z"/><path class="cls-2" d="M14.62,55.74c.92-.61-1.5-.38-1.7-.79a16.49,16.49,0,0,1-2.86-.51c-1.7-.45-5.78-3.19-6.22-5.82S3.84,41,5.68,38a17.21,17.21,0,0,1,4.38-4.7s-3.57-8.49-3.87-14.9S6.39.58,10.57.38s3.57,10.48,2.86,17.43a90.59,90.59,0,0,0-.2,13.88,19.73,19.73,0,0,1,5.5-.92,21.12,21.12,0,0,1,4.9.46s1.33-9.84,5.71-16,9-12.45,12-10.66-.82,8.18-3.47,11.51-7.24,8.33-8.77,11a46.59,46.59,0,0,0-2.65,5.51,14.93,14.93,0,0,1,6.39,6.32C35,43.22,35.16,48.52,34,50.77a8.08,8.08,0,0,1-5.61,4c-1.33.06-6.92,1.45-6.92,1.45Z"/><path class="cls-3" d="M24.41,31.32s3.34-8.71,6.33-13.6,7-8.59,8.26-7.84-2.57,5.32-5.93,9.81a113.44,113.44,0,0,0-7.43,12.38Z"/><path class="cls-3" d="M11,32.81l1-.74s-.14-8.71,0-14,.41-11.9-1.36-11.84-3,5.17-2.31,12.38A87.47,87.47,0,0,0,11,32.81Z"/><circle class="cls-4" cx="8.02" cy="49.54" r="1.94"/><circle class="cls-4" cx="30.95" cy="49.54" r="1.94"/><path class="cls-5" d="M22.7,49.38a.18.18,0,0,0-.25.07s-.58,1-1.42,1.06a2.09,2.09,0,0,1-1.5-.7V48.33c.54-.3,1.41-1.2,1.41-1.51a1.41,1.41,0,0,0-1.49-1.09c-1,0-1.7.65-1.7,1.19s1.1,1.24,1.41,1.44v1.46a1.82,1.82,0,0,1-1.31.69c-.79,0-1.39-1.08-1.4-1.09a.19.19,0,0,0-.25-.08.2.2,0,0,0-.08.26s.71,1.29,1.73,1.29a2.12,2.12,0,0,0,1.51-.73,2.37,2.37,0,0,0,1.58.73h.12a2.49,2.49,0,0,0,1.71-1.26A.18.18,0,0,0,22.7,49.38Z"/><path class="cls-6" d="M10.06,33.32s-2.77-7-3.3-10.66-1-9.77-.3-13.84A50,50,0,0,1,7.68,3.26S6.39,12.75,6.87,16.9,10.06,33.32,10.06,33.32Z"/><path class="cls-6" d="M23.63,31.23s2-10.59,4.91-14.75S35.06,7.27,36.89,6l1.83-1.28s-7.34,7.7-10.18,14S23.63,31.23,23.63,31.23Z"/><path class="cls-5" d="M12,43.23a1.52,1.52,0,1,0,1.51,1.52A1.52,1.52,0,0,0,12,43.23Zm.43,1.41a.41.41,0,0,1-.41-.41.41.41,0,0,1,.82,0A.41.41,0,0,1,12.41,44.64Z"/><path class="cls-5" d="M26.92,43.23a1.52,1.52,0,1,0,1.52,1.52A1.52,1.52,0,0,0,26.92,43.23Zm.43,1.41a.42.42,0,0,1-.41-.41.42.42,0,0,1,.83,0A.42.42,0,0,1,27.35,44.64Z"/><ellipse class="cls-7" cx="19.73" cy="45.32" rx="14.1" ry="10.16"/><path class="cls-2" d="M36.8,39.67V33a2.45,2.45,0,0,0,1.92-2.28,2.65,2.65,0,0,0-5.27,0A2.44,2.44,0,0,0,35.36,33V39.5h-.19c-2-6.94-7.91-11.25-15.62-11.25S5.89,32.52,3.92,39.5H3.25A2.75,2.75,0,0,0,.37,42.09v5.17a2.75,2.75,0,0,0,2.88,2.58h.91a13.11,13.11,0,0,0,3.6,5.09l-1.3,1.16a.79.79,0,0,0-.09,1.12c2.92,3.53,7.71,5.6,13.13,5.6s10.21-2,13.13-5.6a.79.79,0,0,0-.09-1.12l-1.3-1.16a13.34,13.34,0,0,0,3.6-5.09h1a2.75,2.75,0,0,0,2.88-2.58V42.09A2.62,2.62,0,0,0,36.8,39.67ZM19.55,35.62c7.14,0,12.94,4.7,12.94,10.47,0,6.25-6.52,9-12.94,9s-12.94-2.8-12.94-9C6.61,40.32,12.41,35.62,19.55,35.62Z"/><path class="cls-8" d="M37.28,45.53H35.36c0-.3,0-.6,0-.94a19.35,19.35,0,0,0-.38-3.8h.81a1.35,1.35,0,0,1,1.44,1.3Z"/><path class="cls-8" d="M35.84,48.55h-1a16.69,16.69,0,0,0,.43-2.15h2v.86A1.34,1.34,0,0,1,35.84,48.55Z"/><path class="cls-8" d="M19.55,61.48c-4.8,0-9.06-1.73-11.75-4.79l1.35-1.2a17.86,17.86,0,0,0,10.4,3,17.89,17.89,0,0,0,10.4-3l1.34,1.2C28.61,59.75,24.34,61.48,19.55,61.48Z"/><path class="cls-8" d="M3.25,40.79h.86a19.66,19.66,0,0,0-.38,3.8,6.18,6.18,0,0,0,0,.94H1.86V42.09A1.28,1.28,0,0,1,3.25,40.79Z"/><path d="M19.55,34.76a17.4,17.4,0,0,0-4.8.69V30.24a17.14,17.14,0,0,1,4.8-.65,17.12,17.12,0,0,1,4.79.65v5.25A16,16,0,0,0,19.55,34.76Z"/><path class="cls-8" d="M13.8,30.54V35.8C9,37.56,5.65,41.53,5.65,46.09,5.65,52,11.26,56,19.55,56s13.9-4,13.9-9.91c0-4.56-3.36-8.53-8.15-10.29V30.54c5.61,2,9.11,7.19,9.11,14.09,0,7.76-5.95,13-14.86,13s-14.86-5.21-14.86-13C4.69,37.73,8.19,32.56,13.8,30.54Z"/><path class="cls-8" d="M1.81,46.4h2a15.15,15.15,0,0,0,.43,2.15h-1a1.35,1.35,0,0,1-1.44-1.29Z"/><ellipse class="cls-8" cx="36.08" cy="30.67" rx="1.2" ry="1.08"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -19,6 +19,7 @@ import * as api from "@/lib/api";
|
||||
import type { AgentDriverInfo, DriverRuntimeInfo, DriverRuntimeSummary, DriverStoreUsage, JavaRuntimeConfig } from "@/lib/api";
|
||||
import { formatRuntimeBytes, formatRuntimeCpu, formatRuntimeUptime, runtimeHealthClass, runtimeStatusClass, runtimeStatusDotClass } from "@/lib/driverRuntimePresentation";
|
||||
import { addDriverInstallQueue, driverInstallProgressPercent, isDriverInstallProgressTarget, removeDriverInstallQueue, takeNextDriverInstallQueue, type DriverInstallProgress } from "@/lib/driverInstallProgressUi";
|
||||
import { PRESTOSQL_DRIVER_DB_TYPE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/prestoSqlBuiltinDriver";
|
||||
|
||||
const { t } = useI18n();
|
||||
const { toast } = useToast();
|
||||
@@ -261,12 +262,22 @@ function progressTitle(fallback: string): string {
|
||||
return progressText.value || fallback;
|
||||
}
|
||||
|
||||
function isPrestoSqlBuiltinDriver(dbType: string): boolean {
|
||||
return dbType === PRESTOSQL_DRIVER_DB_TYPE;
|
||||
}
|
||||
|
||||
const builtinDriverRows = computed<AgentDriverInfo[]>(() => [...drivers.value, prestoSqlBuiltinDriverRow(jdbcMavenBundles.value)]);
|
||||
|
||||
function driverLabel(dbType: string): string {
|
||||
return builtinDriverRows.value.find((d) => d.db_type === dbType)?.label ?? dbType;
|
||||
}
|
||||
|
||||
function isDriverQueued(dbType: string): boolean {
|
||||
return queuedDriverInstalls.value.includes(dbType);
|
||||
}
|
||||
|
||||
function canInstallOrUpdateDriver(dbType: string): boolean {
|
||||
const driver = drivers.value.find((d) => d.db_type === dbType);
|
||||
const driver = builtinDriverRows.value.find((d) => d.db_type === dbType);
|
||||
return Boolean(driver && (!driver.installed || driver.update_available));
|
||||
}
|
||||
|
||||
@@ -353,10 +364,21 @@ async function installDriver(dbType: string) {
|
||||
}
|
||||
|
||||
async function runDriverInstall(dbType: string) {
|
||||
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
|
||||
const label = driverLabel(dbType);
|
||||
installing.value = dbType;
|
||||
resetInstallProgress();
|
||||
try {
|
||||
if (isPrestoSqlBuiltinDriver(dbType)) {
|
||||
if (!jdbcPluginStatus.value?.installed || !jdbcPluginStatus.value.compatible) {
|
||||
jdbcPluginStatus.value = await api.installJdbcPlugin();
|
||||
emitDriverUpdateCount();
|
||||
}
|
||||
jdbcDrivers.value = await api.installPrestoSqlJdbcDriver();
|
||||
jdbcMavenBundles.value = await api.listJdbcMavenBundles();
|
||||
void loadDriverStoreUsage();
|
||||
toast(t("driverStore.driverInstallSuccess", { label }));
|
||||
return;
|
||||
}
|
||||
const blockers = await api.checkAgentUpdateBlockers([dbType]);
|
||||
if (blockers.length > 0) {
|
||||
toast(t("driverStore.driverUpdateBlocked", { labels: blockers.map((blocker) => blocker.label).join(", ") }));
|
||||
@@ -415,8 +437,17 @@ async function upgradeAll() {
|
||||
}
|
||||
|
||||
async function uninstallDriver(dbType: string) {
|
||||
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
|
||||
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();
|
||||
void loadDriverStoreUsage();
|
||||
toast(t("driverStore.driverUninstallSuccess", { label }));
|
||||
return;
|
||||
}
|
||||
await api.uninstallAgent(dbType);
|
||||
await refreshAgents();
|
||||
toast(t("driverStore.driverUninstallSuccess", { label }));
|
||||
@@ -495,7 +526,11 @@ async function importOfflineZip() {
|
||||
}
|
||||
|
||||
async function importDriverJar(dbType: string) {
|
||||
const label = drivers.value.find((d) => d.db_type === dbType)?.label ?? dbType;
|
||||
if (isPrestoSqlBuiltinDriver(dbType)) {
|
||||
await importJdbcDrivers();
|
||||
return;
|
||||
}
|
||||
const label = driverLabel(dbType);
|
||||
if (isWeb) {
|
||||
const file = await chooseWebFile(".jar");
|
||||
if (!file) return;
|
||||
@@ -600,8 +635,8 @@ type JdbcDriverListItem =
|
||||
|
||||
const filteredAgentDrivers = computed(() => {
|
||||
const query = agentDriverSearch.value.trim().toLowerCase();
|
||||
if (!query) return drivers.value;
|
||||
return drivers.value.filter((driver) => [driver.label, driver.db_type, driver.version, driver.installed_version, driver.jre].filter(Boolean).join(" ").toLowerCase().includes(query));
|
||||
if (!query) return builtinDriverRows.value;
|
||||
return builtinDriverRows.value.filter((driver) => [driver.label, driver.db_type, driver.version, driver.installed_version, driver.jre].filter(Boolean).join(" ").toLowerCase().includes(query));
|
||||
});
|
||||
|
||||
const jdbcDriverListItems = computed<JdbcDriverListItem[]>(() => {
|
||||
@@ -1102,7 +1137,7 @@ watch(driverStoreTab, (tab) => {
|
||||
{{ t("driverStore.install") }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!driver.installed && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
|
||||
v-if="!driver.installed && !isPrestoSqlBuiltinDriver(driver.db_type) && !isDriverProgressActive(driver.db_type) && !isDriverQueued(driver.db_type)"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
class="h-7 w-7 rounded-full text-xs text-muted-foreground"
|
||||
|
||||
@@ -484,6 +484,7 @@ const driverProfiles: Record<
|
||||
h2: { type: "h2", port: 9092, user: "sa", label: "H2", icon: "h2" },
|
||||
snowflake: { type: "snowflake", port: 443, user: "", label: "Snowflake", icon: "snowflake" },
|
||||
trino: { type: "trino", port: 8080, user: "", label: "Trino", icon: "trino" },
|
||||
prestosql: { type: "prestosql", port: 8080, user: "", label: "PrestoSQL", icon: "presto" },
|
||||
hive: { type: "hive", port: 10000, user: "", label: "Apache Hive", icon: "hive" },
|
||||
db2: { type: "db2", port: 50000, user: "db2inst1", label: "IBM DB2", icon: "db2" },
|
||||
informix: { type: "informix", port: 9088, user: "informix", label: "Informix", icon: "informix" },
|
||||
@@ -674,6 +675,13 @@ function applyProfile(val: string, preserveConnectionFields = false) {
|
||||
form.value.jdbc_driver_paths = [];
|
||||
jdbcDriverPathsInput.value = "";
|
||||
}
|
||||
if (profile.type === "prestosql") {
|
||||
form.value.connection_string = undefined;
|
||||
form.value.jdbc_driver_class = "io.prestosql.jdbc.PrestoDriver";
|
||||
form.value.jdbc_driver_paths = [];
|
||||
jdbcDriverPathsInput.value = "";
|
||||
jdbcManualClasspathOpen.value = true;
|
||||
}
|
||||
if (profile.type === "mq") {
|
||||
resetMqFields();
|
||||
form.value.database = undefined;
|
||||
@@ -763,6 +771,7 @@ watch(
|
||||
}
|
||||
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;
|
||||
customDriverName.value = isCustomCompatibleProfile() ? config.driver_label || "" : "";
|
||||
dialogStep.value = "config";
|
||||
configTab.value = "connection";
|
||||
@@ -913,6 +922,7 @@ const iconTypeMap: Record<string, string> = {
|
||||
h2: "h2",
|
||||
snowflake: "snowflake",
|
||||
trino: "trino",
|
||||
prestosql: "prestosql",
|
||||
hive: "hive",
|
||||
db2: "db2",
|
||||
informix: "informix",
|
||||
@@ -977,6 +987,7 @@ const dbOptions: DbOption[] = [
|
||||
{ value: "h2", label: "H2" },
|
||||
{ value: "snowflake", label: "Snowflake" },
|
||||
{ value: "trino", label: "Trino" },
|
||||
{ value: "prestosql", label: "PrestoSQL" },
|
||||
{ value: "hive", label: "Hive" },
|
||||
{ value: "db2", label: "DB2" },
|
||||
{ value: "informix", label: "Informix" },
|
||||
@@ -1022,7 +1033,9 @@ const filteredDbCategories = computed<DbCategory[]>(() => {
|
||||
|
||||
const hasDbPickerResults = computed(() => filteredDbCategories.value.some((category) => category.options.length > 0));
|
||||
const selectedDbIcon = computed(() => iconTypeMap[selectedType.value] || selectedProfile().icon || selectedType.value);
|
||||
const jdbcBackedDatabaseTypes = new Set<DatabaseType>(["jdbc", "prestosql"]);
|
||||
const isJdbcConnection = computed(() => form.value.db_type === "jdbc");
|
||||
const isPrestoSqlConnection = computed(() => form.value.db_type === "prestosql");
|
||||
const isH2FileMode = computed(() => form.value.db_type === "h2" && h2ConnectionMode.value === "file");
|
||||
const usesLocalFilePathInput = computed(() => isLocalFileTypeDb(form.value.db_type) && (form.value.db_type !== "h2" || isH2FileMode.value));
|
||||
|
||||
@@ -1369,10 +1382,15 @@ function connectionConfigForSubmit(id: string): ConnectionConfig {
|
||||
} else {
|
||||
config.ca_cert_path = config.ca_cert_path?.trim() || "";
|
||||
}
|
||||
if (config.db_type === "jdbc") {
|
||||
config.host = "";
|
||||
config.port = 0;
|
||||
config.connection_string = config.connection_string?.trim() || "";
|
||||
if (jdbcBackedDatabaseTypes.has(config.db_type)) {
|
||||
if (config.db_type === "jdbc") {
|
||||
config.host = "";
|
||||
config.port = 0;
|
||||
config.connection_string = config.connection_string?.trim() || "";
|
||||
} else if (config.db_type === "prestosql") {
|
||||
config.connection_string = undefined;
|
||||
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || "io.prestosql.jdbc.PrestoDriver";
|
||||
}
|
||||
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || undefined;
|
||||
config.jdbc_driver_paths = jdbcDriverPathsInput.value
|
||||
.split(/\r?\n/)
|
||||
@@ -2497,7 +2515,7 @@ function openExternalUrl(url: string) {
|
||||
</div>
|
||||
|
||||
<!-- JDBC: optional external plugin -->
|
||||
<template v-if="form.db_type === 'jdbc'">
|
||||
<template v-if="isJdbcConnection">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label class="text-right">{{ t("connection.jdbcUrl") }}</Label>
|
||||
<Input v-model="form.connection_string" class="col-span-3" :placeholder="t('connection.jdbcUrlPlaceholder')" />
|
||||
@@ -2560,6 +2578,10 @@ function openExternalUrl(url: string) {
|
||||
{{ t("connection.jdbcPluginHint") }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="outline" size="sm" @click="emit('openDriverStore')">
|
||||
<FolderOpen class="h-3.5 w-3.5" />
|
||||
{{ t("toolbar.driverManager") }}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" @click="openExternalUrl('https://dbxio.com')">
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
{{ t("connection.jdbcDocs") }}
|
||||
@@ -3071,6 +3093,70 @@ function openExternalUrl(url: string) {
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template v-if="isPrestoSqlConnection">
|
||||
<div class="grid grid-cols-4 items-start gap-4">
|
||||
<Label class="text-right mt-2">{{ t("connection.jdbcDriverPaths") }}</Label>
|
||||
<div class="col-span-3 space-y-2">
|
||||
<Select v-if="jdbcDriverSelectItems.length > 0" :model-value="selectedJdbcDriverPath" @update:model-value="onJdbcDriverSelect">
|
||||
<SelectTrigger>
|
||||
<SelectValue :placeholder="t('connection.jdbcDriverSelectPlaceholder')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="driver in jdbcDriverSelectItems" :key="driver.id" :value="driver.id">
|
||||
{{ driver.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div class="flex items-center justify-between gap-3 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<div class="truncate text-xs font-medium">{{ t("connection.jdbcManualClasspath") }}</div>
|
||||
<Badge variant="outline" class="h-5 shrink-0 rounded-full px-2 text-[10px] font-medium">
|
||||
{{ t("connection.jdbcManualClasspathCount", { count: jdbcManualClasspathCount }) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<Switch v-model="jdbcManualClasspathOpen" />
|
||||
</div>
|
||||
<div v-if="jdbcManualClasspathOpen" class="flex items-start gap-1">
|
||||
<textarea
|
||||
v-model="jdbcDriverPathsInput"
|
||||
class="flex min-h-12 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
:placeholder="t('connection.jdbcDriverPathsPlaceholder')"
|
||||
/>
|
||||
<Tooltip v-if="isDesktop">
|
||||
<TooltipTrigger as-child>
|
||||
<Button type="button" variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="browseJdbcDriverPaths">
|
||||
<FolderOpen class="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("connection.jdbcDriverBrowse") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label class="text-right">{{ t("connection.jdbcDriverClass") }}</Label>
|
||||
<Input v-model="form.jdbc_driver_class" class="col-span-3" :placeholder="t('connection.jdbcDriverClassPlaceholder')" />
|
||||
</div>
|
||||
<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">
|
||||
{{ t("connection.jdbcPluginHint") }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button type="button" variant="outline" size="sm" @click="emit('openDriverStore')">
|
||||
<FolderOpen class="h-3.5 w-3.5" />
|
||||
{{ t("toolbar.driverManager") }}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" @click="openExternalUrl('https://dbxio.com')">
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
{{ t("connection.jdbcDocs") }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -40,6 +40,7 @@ import { trimmedSelectionLayer } from "@/lib/codemirrorTrimmedSelectionLayer";
|
||||
import { selectionMatchOccurrences } from "@/lib/codemirrorSelectionMatches";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/codemirrorSqlDialect";
|
||||
import { isSchemaAware, isSingleDatabase } from "@/lib/databaseFeatureSupport";
|
||||
import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/completionMetadataPolicy";
|
||||
import { qualifiedTableNameAtSqlPosition } from "@/lib/queryCursorTableTarget";
|
||||
import * as api from "@/lib/api";
|
||||
import { areSqlSemanticDiagnosticsEqual, buildSqlParserErrorDiagnostic, buildSqlSemanticDiagnostics, shouldRunSqlSemanticDiagnostics, type SqlSemanticDiagnostic } from "@/lib/sqlSemanticDiagnostics";
|
||||
@@ -579,6 +580,14 @@ function supportsDatabaseQualifierCompletion(): boolean {
|
||||
return !!props.databaseType && !isSchemaAware(props.databaseType) && !isSingleDatabase(props.databaseType);
|
||||
}
|
||||
|
||||
function usesLocalOnlyCompletionMetadata(): boolean {
|
||||
return usesLocalOnlyEditorCompletionMetadata(props.databaseType);
|
||||
}
|
||||
|
||||
function usesOnDemandOnlyCompletionColumns(): boolean {
|
||||
return usesOnDemandOnlyEditorColumnMetadata(props.databaseType);
|
||||
}
|
||||
|
||||
function completionMetadataTarget(table: { name: string; schema?: string | null }): { database: string; schema?: string } | null {
|
||||
if (props.database == null) return null;
|
||||
if (supportsDatabaseQualifierCompletion() && table.schema) {
|
||||
@@ -711,11 +720,13 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
|
||||
|
||||
try {
|
||||
if (cachedTables.length === 0) {
|
||||
cachedTables = await connectionStore.listCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema);
|
||||
cachedTables = usesLocalOnlyCompletionMetadata()
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema)
|
||||
: await connectionStore.listCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema);
|
||||
}
|
||||
|
||||
let table = matchTable(identifier, cachedTables) ?? matchTable(name, cachedTables);
|
||||
if (!table) {
|
||||
if (!table && !usesLocalOnlyCompletionMetadata()) {
|
||||
const hoverTables = await connectionStore.listCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema);
|
||||
cachedTables = [...cachedTables, ...hoverTables];
|
||||
table = matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
|
||||
@@ -824,6 +835,12 @@ async function enrichSemanticDiagnosticTables(tables: SqlTableReference[]) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (usesLocalOnlyCompletionMetadata()) {
|
||||
const matches = connectionStore.lookupLocalCompletionTables(props.connectionId, props.database, table.name, MAX_COMPLETION_TABLES, props.schema);
|
||||
const match = matches.find((item) => item.name.toLowerCase() === table.name.toLowerCase());
|
||||
enriched.push(match?.schema ? { ...table, schema: match.schema } : table);
|
||||
continue;
|
||||
}
|
||||
const matches = await connectionStore.listCompletionTables(props.connectionId, props.database, table.name, MAX_COMPLETION_TABLES, props.schema);
|
||||
cachedTables = [...cachedTables, ...matches];
|
||||
const match = matches.find((item) => item.name.toLowerCase() === table.name.toLowerCase());
|
||||
@@ -875,7 +892,9 @@ async function refreshSemanticDiagnostics() {
|
||||
if (runId !== semanticDiagnosticRunId) return;
|
||||
|
||||
const tables = await enrichSemanticDiagnosticTables(analysis.tables);
|
||||
await Promise.all(tables.map((table) => ensureColumnsForTable(table)));
|
||||
if (!usesOnDemandOnlyCompletionColumns()) {
|
||||
await Promise.all(tables.map((table) => ensureColumnsForTable(table)));
|
||||
}
|
||||
if (runId !== semanticDiagnosticRunId) return;
|
||||
|
||||
const enrichedAnalysis: SqlReferenceAnalysis = { ...analysis, tables };
|
||||
@@ -1359,10 +1378,12 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType<typeof getS
|
||||
|
||||
function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof getSqlCompletionContext>) {
|
||||
if (!props.connectionId || props.database == null) return;
|
||||
const localOnlyMetadata = usesLocalOnlyCompletionMetadata();
|
||||
const onDemandOnlyColumns = usesOnDemandOnlyCompletionColumns();
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
const schema = completionContext.qualifier && completionContext.suggestTables ? completionContext.qualifier : props.schema;
|
||||
if (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext))) {
|
||||
if (!localOnlyMetadata && (completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext)))) {
|
||||
void connectionStore
|
||||
.refreshCompletionTables(connectionId, database, completionContext.qualifier && !schema ? completionContext.qualifier : completionContext.prefix, MAX_COMPLETION_TABLES, schema)
|
||||
.then((tables) => {
|
||||
@@ -1373,7 +1394,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
if (completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions)) {
|
||||
if (!localOnlyMetadata && (completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions))) {
|
||||
void connectionStore
|
||||
.refreshCompletionObjects(connectionId, database, completionContext.prefix, MAX_COMPLETION_TABLES, props.schema)
|
||||
.then((objects) => {
|
||||
@@ -1381,13 +1402,13 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
if (!localOnlyMetadata && completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
void connectionStore.refreshCompletionSchemas(connectionId, database).catch(() => {});
|
||||
if (supportsDatabaseQualifierCompletion()) {
|
||||
void connectionStore.refreshCompletionDatabases(connectionId).catch(() => {});
|
||||
}
|
||||
}
|
||||
if (completionContext.insertTable) {
|
||||
if (!onDemandOnlyColumns && completionContext.insertTable) {
|
||||
const insertTable = completionContext.insertTable;
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, database, insertTable, completionContext.insertSchema ?? props.schema)
|
||||
@@ -1399,7 +1420,7 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
}
|
||||
const qualifiedColumnTarget = completionQualifiedTableTarget(completionContext);
|
||||
const qualifiedColumnCacheKey = qualifiedColumnTarget ? completionCacheKey(qualifiedColumnTarget) : undefined;
|
||||
if (qualifiedColumnTarget && qualifiedColumnCacheKey && !cachedColumnsByTable.has(qualifiedColumnCacheKey)) {
|
||||
if (!onDemandOnlyColumns && qualifiedColumnTarget && qualifiedColumnCacheKey && !cachedColumnsByTable.has(qualifiedColumnCacheKey)) {
|
||||
const target = completionMetadataTarget(qualifiedColumnTarget);
|
||||
if (target) {
|
||||
void connectionStore
|
||||
@@ -1410,19 +1431,21 @@ function scheduleCompletionMetadataRefresh(completionContext: ReturnType<typeof
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
for (const refTable of completionContext.referencedTables) {
|
||||
if (refTable.columns && refTable.columns.length > 0) continue;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cacheKey === qualifiedColumnCacheKey) continue;
|
||||
if (cachedColumnsByTable.has(cacheKey)) continue;
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) continue;
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, target.database, refTable.name, target.schema)
|
||||
.then((columns) => {
|
||||
if (columns.length > 0) cachedColumnsByTable.set(cacheKey, columns);
|
||||
})
|
||||
.catch(() => {});
|
||||
if (!onDemandOnlyColumns) {
|
||||
for (const refTable of completionContext.referencedTables) {
|
||||
if (refTable.columns && refTable.columns.length > 0) continue;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cacheKey === qualifiedColumnCacheKey) continue;
|
||||
if (cachedColumnsByTable.has(cacheKey)) continue;
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) continue;
|
||||
void connectionStore
|
||||
.refreshCompletionColumns(connectionId, target.database, refTable.name, target.schema)
|
||||
.then((columns) => {
|
||||
if (columns.length > 0) cachedColumnsByTable.set(cacheKey, columns);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
if ((completionContext.suggestTables || completionContext.suggestJoinConditions) && completionContext.referencedTables.length > 0) {
|
||||
void ensureForeignKeysForTables(completionContext.referencedTables);
|
||||
@@ -1442,6 +1465,8 @@ function mergeCompletionTables(existing: Array<{ name: string; schema?: string;
|
||||
}
|
||||
|
||||
async function performAsyncCompletionWithResult(epoch: number, completionContext: ReturnType<typeof getSqlCompletionContext>, fullDoc: string, position: number) {
|
||||
const localOnlyMetadata = usesLocalOnlyCompletionMetadata();
|
||||
const onDemandOnlyColumns = usesOnDemandOnlyCompletionColumns();
|
||||
// Handle INSERT column list: fetch columns for the target table
|
||||
let insertColumnsByTable = new Map<string, SqlCompletionColumn[]>();
|
||||
if (completionContext.insertTable) {
|
||||
@@ -1460,7 +1485,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
|
||||
const shouldLoadTables = completionContext.suggestTables || (!!completionContext.qualifier && !isReferencedTableQualifier(completionContext));
|
||||
let databaseNames: string[] = [];
|
||||
if (supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable) {
|
||||
if (!localOnlyMetadata && supportsDatabaseQualifierCompletion() && completionContext.suggestTables && !completionContext.insertTable) {
|
||||
try {
|
||||
databaseNames = await connectionStore.listCompletionDatabases(props.connectionId!);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
@@ -1472,14 +1497,22 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
const tableLookupDatabase = qualifierDatabase ?? props.database!;
|
||||
const tableLookupSchema = qualifierDatabase ? undefined : props.schema;
|
||||
const tableLookupFilter = completionContext.qualifier && !qualifierDatabase ? completionContext.qualifier : completionContext.prefix;
|
||||
let tables = shouldLoadTables ? await connectionStore.listCompletionTables(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema) : cachedTables;
|
||||
let tables = shouldLoadTables
|
||||
? localOnlyMetadata
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema)
|
||||
: await connectionStore.listCompletionTables(props.connectionId!, tableLookupDatabase, tableLookupFilter, MAX_COMPLETION_TABLES, tableLookupSchema)
|
||||
: cachedTables;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
const shouldLoadObjects = completionContext.suggestRoutines || completionContext.exclusiveRoutineSuggestions || (!!completionContext.qualifier && !completionContext.exclusiveColumnSuggestions);
|
||||
let completionObjects = shouldLoadObjects ? await connectionStore.listCompletionObjects(props.connectionId!, props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema) : cachedCompletionObjects;
|
||||
let completionObjects = shouldLoadObjects
|
||||
? localOnlyMetadata
|
||||
? connectionStore.lookupLocalCompletionObjects(props.connectionId!, props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema)
|
||||
: await connectionStore.listCompletionObjects(props.connectionId!, props.database!, completionContext.qualifier || completionContext.prefix, MAX_COMPLETION_TABLES, props.schema)
|
||||
: cachedCompletionObjects;
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
if (completionContext.qualifier && completionObjects.length === 0) {
|
||||
if (!localOnlyMetadata && completionContext.qualifier && completionObjects.length === 0) {
|
||||
const schemaObjects = await connectionStore.listCompletionObjects(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
|
||||
if (schemaObjects.length > 0) {
|
||||
completionObjects = schemaObjects;
|
||||
@@ -1491,19 +1524,25 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
// Fetch schemas for schema completion
|
||||
let schemaNames: string[] = [];
|
||||
if (completionContext.suggestTables && !completionContext.qualifier && !completionContext.insertTable) {
|
||||
try {
|
||||
const schemas = await connectionStore.listCompletionSchemas(props.connectionId!, props.database!);
|
||||
schemaNames = mergeCompletionQualifierNames(schemas, databaseNames);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
schemaNames = databaseNames;
|
||||
if (localOnlyMetadata) {
|
||||
schemaNames = mergeCompletionQualifierNames(connectionStore.lookupLocalCompletionSchemas(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES), databaseNames);
|
||||
} else {
|
||||
try {
|
||||
const schemas = await connectionStore.listCompletionSchemas(props.connectionId!, props.database!);
|
||||
schemaNames = mergeCompletionQualifierNames(schemas, databaseNames);
|
||||
if (epoch !== completionEpoch) return null;
|
||||
} catch {
|
||||
schemaNames = databaseNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If qualifier didn't match any table names, try it as a schema name
|
||||
let qualifierIsSchema = false;
|
||||
if (completionContext.qualifier && !qualifierDatabase && !isReferencedTableQualifier(completionContext) && tables.length === 0 && (completionContext.suggestTables || completionContext.exclusiveColumnSuggestions)) {
|
||||
const schemaTables = await connectionStore.listCompletionTables(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
|
||||
const schemaTables = localOnlyMetadata
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier)
|
||||
: await connectionStore.listCompletionTables(props.connectionId!, props.database!, completionContext.prefix, MAX_COMPLETION_TABLES, completionContext.qualifier);
|
||||
if (schemaTables.length > 0) {
|
||||
tables = schemaTables;
|
||||
qualifierIsSchema = true;
|
||||
@@ -1522,7 +1561,7 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
return rt;
|
||||
});
|
||||
const unresolvedRefs = refs.filter((rt) => !rt.schema && !rt.columns);
|
||||
if (unresolvedRefs.length > 0) {
|
||||
if (!localOnlyMetadata && unresolvedRefs.length > 0) {
|
||||
const lookupGroups = await Promise.all(unresolvedRefs.map((rt) => connectionStore.listCompletionTables(props.connectionId!, props.database!, rt.name, 20, props.schema)));
|
||||
if (epoch !== completionEpoch) return null;
|
||||
const lookupTables = lookupGroups.flat();
|
||||
@@ -1555,23 +1594,26 @@ async function performAsyncCompletionWithResult(epoch: number, completionContext
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
refs.map(async (refTable) => {
|
||||
if (refTable.columns && refTable.columns.length > 0) return;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cachedColumnsByTable.has(cacheKey)) return;
|
||||
try {
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) return;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId!, target.database, refTable.name, target.schema);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
} catch (e) {
|
||||
console.error(`[DBX] Failed to load columns for ${cacheKey}:`, e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
const shouldFetchColumnsForCompletion = !onDemandOnlyColumns || completionContext.suggestColumns || completionContext.exclusiveColumnSuggestions || !!completionContext.insertTable;
|
||||
if (shouldFetchColumnsForCompletion) {
|
||||
await Promise.all(
|
||||
refs.map(async (refTable) => {
|
||||
if (refTable.columns && refTable.columns.length > 0) return;
|
||||
const cacheKey = refTable.schema ? `${refTable.schema}.${refTable.name}` : refTable.name;
|
||||
if (cachedColumnsByTable.has(cacheKey)) return;
|
||||
try {
|
||||
const target = completionMetadataTarget(refTable);
|
||||
if (!target) return;
|
||||
const columns = await connectionStore.listCompletionColumns(props.connectionId!, target.database, refTable.name, target.schema);
|
||||
if (epoch !== completionEpoch) return;
|
||||
if (columns.length === 0) return;
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
} catch (e) {
|
||||
console.error(`[DBX] Failed to load columns for ${cacheKey}:`, e);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (epoch !== completionEpoch) return null;
|
||||
|
||||
if ((completionContext.suggestTables || completionContext.suggestJoinConditions) && refs.length > 0) {
|
||||
@@ -2004,7 +2046,9 @@ onMounted(async () => {
|
||||
try {
|
||||
// Ensure table cache is populated
|
||||
if (cachedTables.length === 0) {
|
||||
cachedTables = await connectionStore.listCompletionTables(props.connectionId!, props.database!, identifier, MAX_COMPLETION_TABLES, props.schema);
|
||||
cachedTables = usesLocalOnlyCompletionMetadata()
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId!, props.database!, identifier, MAX_COMPLETION_TABLES, props.schema)
|
||||
: await connectionStore.listCompletionTables(props.connectionId!, props.database!, identifier, MAX_COMPLETION_TABLES, props.schema);
|
||||
}
|
||||
|
||||
// 1. Check if it's a table name
|
||||
|
||||
@@ -44,10 +44,11 @@ const assetIcons: Record<string, string> = {
|
||||
dm: "dm",
|
||||
dameng: "dm",
|
||||
presto: "presto",
|
||||
prestosql: "presto",
|
||||
hive: "hive",
|
||||
apache_kylin: "apache_kylin",
|
||||
sundb: "sundb",
|
||||
trino: "presto",
|
||||
trino: "trino",
|
||||
kylin: "apache_kylin",
|
||||
cockroachdb: "cockroachdb",
|
||||
db2: "db2",
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/completionMetadataPolicy";
|
||||
|
||||
describe("completionMetadataPolicy", () => {
|
||||
it("keeps Presto-like editor completion on local metadata", () => {
|
||||
expect(usesLocalOnlyEditorCompletionMetadata("prestosql")).toBe(true);
|
||||
expect(usesLocalOnlyEditorCompletionMetadata("trino")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not change regular database completion behavior", () => {
|
||||
expect(usesLocalOnlyEditorCompletionMetadata("mysql")).toBe(false);
|
||||
expect(usesLocalOnlyEditorCompletionMetadata("postgres")).toBe(false);
|
||||
expect(usesLocalOnlyEditorCompletionMetadata("jdbc")).toBe(false);
|
||||
expect(usesLocalOnlyEditorCompletionMetadata(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps Presto-like editor column metadata on demand only", () => {
|
||||
expect(usesOnDemandOnlyEditorColumnMetadata("prestosql")).toBe(true);
|
||||
expect(usesOnDemandOnlyEditorColumnMetadata("trino")).toBe(true);
|
||||
expect(usesOnDemandOnlyEditorColumnMetadata("mysql")).toBe(false);
|
||||
expect(usesOnDemandOnlyEditorColumnMetadata(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/completionTreeIndex";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
|
||||
describe("completionTreeIndex", () => {
|
||||
it("extracts PrestoSQL schemas and loaded grouped tables from the sidebar tree", () => {
|
||||
const tree: TreeNode[] = [
|
||||
{
|
||||
id: "conn",
|
||||
label: "PrestoSQL",
|
||||
type: "connection",
|
||||
connectionId: "conn",
|
||||
children: [
|
||||
{
|
||||
id: "conn:hive",
|
||||
label: "hive",
|
||||
type: "database",
|
||||
connectionId: "conn",
|
||||
database: "hive",
|
||||
children: [
|
||||
{
|
||||
id: "conn:hive:sales_analytics",
|
||||
label: "sales_analytics",
|
||||
type: "schema",
|
||||
connectionId: "conn",
|
||||
database: "hive",
|
||||
schema: "sales_analytics",
|
||||
children: [
|
||||
{
|
||||
id: "conn:hive:sales_analytics:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "conn",
|
||||
database: "hive",
|
||||
schema: "sales_analytics",
|
||||
children: [
|
||||
{
|
||||
id: "conn:hive:sales_analytics:__tables:daily_revenue",
|
||||
label: "daily_revenue",
|
||||
type: "table",
|
||||
connectionId: "conn",
|
||||
database: "hive",
|
||||
schema: "sales_analytics",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
expect(completionSchemasFromTree(tree, "conn", "hive")).toEqual(["sales_analytics"]);
|
||||
expect(completionTablesFromTree(tree, "conn", "hive", "sales_analytics")).toEqual([{ name: "daily_revenue", schema: "sales_analytics", type: "table" }]);
|
||||
expect(completionTablesFromTree(tree, "conn", "hive")).toEqual([{ name: "daily_revenue", schema: "sales_analytics", type: "table" }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { PRESTOSQL_JDBC_DRIVER_COORDINATE, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/prestoSqlBuiltinDriver";
|
||||
import type { JdbcMavenBundleInfo } from "@/types/database";
|
||||
|
||||
function bundle(coordinate: string, version = "350"): JdbcMavenBundleInfo {
|
||||
return {
|
||||
id: `bundle:${coordinate}`,
|
||||
coordinate,
|
||||
scope: "runtime",
|
||||
repositories: ["https://repo.maven.apache.org/maven2/"],
|
||||
installed_at: "2026-06-18T00:00:00Z",
|
||||
path: "drivers/jdbc",
|
||||
artifacts: [
|
||||
{
|
||||
group_id: "io.prestosql",
|
||||
artifact_id: "presto-jdbc",
|
||||
version,
|
||||
classifier: "",
|
||||
extension: "jar",
|
||||
file_name: `presto-jdbc-${version}.jar`,
|
||||
path: `drivers/jdbc/presto-jdbc-${version}.jar`,
|
||||
size: 12_345,
|
||||
sha256: "abc",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("prestoSqlBuiltinDriver", () => {
|
||||
it("adds an uninstalled PrestoSQL row when the Maven bundle is absent", () => {
|
||||
const row = prestoSqlBuiltinDriverRow([]);
|
||||
|
||||
expect(row.db_type).toBe("prestosql");
|
||||
expect(row.label).toBe("PrestoSQL");
|
||||
expect(row.version).toBe("350");
|
||||
expect(row.installed).toBe(false);
|
||||
expect(row.installed_version).toBeNull();
|
||||
expect(row.jre).toBe("");
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
expect(prestoSqlMavenBundle([installed])?.id).toBe(installed.id);
|
||||
expect(row.installed).toBe(true);
|
||||
expect(row.installed_version).toBe("350");
|
||||
});
|
||||
});
|
||||
@@ -66,6 +66,7 @@ export const listJdbcDrivers = forward("listJdbcDrivers");
|
||||
export const listJdbcMavenBundles = forward("listJdbcMavenBundles");
|
||||
export const importJdbcDrivers = forward("importJdbcDrivers");
|
||||
export const installJdbcDriverFromMaven = forward("installJdbcDriverFromMaven");
|
||||
export const installPrestoSqlJdbcDriver = forward("installPrestoSqlJdbcDriver");
|
||||
export const deleteJdbcDriver = forward("deleteJdbcDriver");
|
||||
export const deleteJdbcMavenBundle = forward("deleteJdbcMavenBundle");
|
||||
export const jdbcPluginStatus = forward("jdbcPluginStatus");
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
const LOCAL_ONLY_EDITOR_COMPLETION_METADATA_TYPES = new Set<DatabaseType>(["prestosql", "trino"]);
|
||||
|
||||
export function usesLocalOnlyEditorCompletionMetadata(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && LOCAL_ONLY_EDITOR_COMPLETION_METADATA_TYPES.has(databaseType);
|
||||
}
|
||||
|
||||
export function usesOnDemandOnlyEditorColumnMetadata(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && LOCAL_ONLY_EDITOR_COMPLETION_METADATA_TYPES.has(databaseType);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import type { SqlCompletionTable } from "@/lib/sqlCompletion";
|
||||
|
||||
const TABLE_NODE_TYPES = new Set(["table", "view", "materialized_view"]);
|
||||
|
||||
export function completionSchemasFromTree(nodes: readonly TreeNode[], connectionId: string, database: string): string[] {
|
||||
const seen = new Set<string>();
|
||||
const schemas: string[] = [];
|
||||
visitTree(nodes, (node) => {
|
||||
if (node.connectionId !== connectionId || node.database !== database || node.type !== "schema" || !node.schema) return;
|
||||
const key = node.schema.toLowerCase();
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
schemas.push(node.schema);
|
||||
});
|
||||
return schemas;
|
||||
}
|
||||
|
||||
export function completionTablesFromTree(nodes: readonly TreeNode[], connectionId: string, database: string, schema?: string): SqlCompletionTable[] {
|
||||
const preferredSchema = schema?.toLowerCase();
|
||||
const tables: SqlCompletionTable[] = [];
|
||||
visitTree(nodes, (node) => {
|
||||
if (node.connectionId !== connectionId || node.database !== database || !TABLE_NODE_TYPES.has(node.type)) return;
|
||||
if (preferredSchema && node.schema?.toLowerCase() !== preferredSchema) return;
|
||||
tables.push({
|
||||
name: node.tableName || node.label,
|
||||
schema: node.schema,
|
||||
type: node.type === "view" || node.type === "materialized_view" ? "view" : "table",
|
||||
});
|
||||
});
|
||||
return dedupeCompletionTables(tables);
|
||||
}
|
||||
|
||||
function visitTree(nodes: readonly TreeNode[], visit: (node: TreeNode) => void) {
|
||||
for (const node of nodes) {
|
||||
visit(node);
|
||||
if (node.children?.length) visitTree(node.children, visit);
|
||||
if (node.hiddenChildren?.length) visitTree(node.hiddenChildren, visit);
|
||||
}
|
||||
}
|
||||
|
||||
function dedupeCompletionTables(tables: SqlCompletionTable[]): SqlCompletionTable[] {
|
||||
const seen = new Set<string>();
|
||||
const result: SqlCompletionTable[] = [];
|
||||
for (const table of tables) {
|
||||
const key = `${table.schema ?? ""}.${table.name}`.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(table);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -26,6 +26,7 @@ const AGENT_DRIVER_TYPES = new Set<DatabaseType>([
|
||||
"h2",
|
||||
"snowflake",
|
||||
"trino",
|
||||
"prestosql",
|
||||
"hive",
|
||||
"db2",
|
||||
"informix",
|
||||
|
||||
@@ -24,6 +24,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
|
||||
"h2",
|
||||
"snowflake",
|
||||
"trino",
|
||||
"prestosql",
|
||||
"hive",
|
||||
"databend",
|
||||
"db2",
|
||||
@@ -60,6 +61,7 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
|
||||
"gbase",
|
||||
"jdbc",
|
||||
"trino",
|
||||
"prestosql",
|
||||
"h2",
|
||||
"informix",
|
||||
"tdengine",
|
||||
|
||||
@@ -40,6 +40,7 @@ const DATABASE_TYPE_OBJECTS = new Map<DatabaseType, SidebarObjectKind[]>([
|
||||
["starrocks", TABLE_VIEW_OBJECTS],
|
||||
["hive", TABLE_VIEW_OBJECTS],
|
||||
["trino", TABLE_VIEW_OBJECTS],
|
||||
["prestosql", TABLE_VIEW_OBJECTS],
|
||||
["cassandra", TABLE_VIEW_OBJECTS],
|
||||
["bigquery", TABLE_VIEW_OBJECTS],
|
||||
["kylin", TABLE_VIEW_OBJECTS],
|
||||
|
||||
@@ -131,6 +131,15 @@ const DATABASE_CAPABILITY_OVERRIDES: Partial<Record<DatabaseType, Partial<Databa
|
||||
transaction: false,
|
||||
},
|
||||
},
|
||||
prestosql: {
|
||||
tableData: {
|
||||
insert: true,
|
||||
updateRequiresPrimaryKey: true,
|
||||
deleteRequiresPrimaryKey: true,
|
||||
requiresTransactionalTableForExistingRows: false,
|
||||
transaction: false,
|
||||
},
|
||||
},
|
||||
clickhouse: {
|
||||
tableData: {
|
||||
insert: false,
|
||||
|
||||
@@ -228,6 +228,10 @@ export async function installJdbcDriverFromMaven(coordinate: string, repositorie
|
||||
return post("/api/jdbc/drivers/maven", { coordinate, repositories });
|
||||
}
|
||||
|
||||
export async function installPrestoSqlJdbcDriver(): Promise<JdbcDriverInfo[]> {
|
||||
return post("/api/jdbc/drivers/prestosql", {});
|
||||
}
|
||||
|
||||
export async function deleteJdbcDriver(path: string): Promise<JdbcDriverInfo[]> {
|
||||
const fileName = path.split("/").pop() || path;
|
||||
return del(`/api/jdbc/drivers/${encodeURIComponent(fileName)}`);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { AgentDriverInfo } from "@/lib/api";
|
||||
import type { JdbcMavenBundleInfo } from "@/types/database";
|
||||
|
||||
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 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,
|
||||
jre: "",
|
||||
jre_installed: true,
|
||||
};
|
||||
}
|
||||
@@ -899,6 +899,10 @@ export async function installJdbcDriverFromMaven(coordinate: string, repositorie
|
||||
return invoke("install_jdbc_driver_from_maven", { request: { coordinate, repositories } });
|
||||
}
|
||||
|
||||
export async function installPrestoSqlJdbcDriver(): Promise<JdbcDriverInfo[]> {
|
||||
return invoke("install_prestosql_jdbc_driver");
|
||||
}
|
||||
|
||||
export async function deleteJdbcDriver(path: string): Promise<JdbcDriverInfo[]> {
|
||||
return invoke("delete_jdbc_driver", { path });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { uuid } from "@/lib/utils";
|
||||
import { ref, computed, watch } from "vue";
|
||||
import type { ColumnInfo, ConnectionConfig, ForeignKeyInfo, ObjectInfo, SidebarLayout, TreeNode } from "@/types/database";
|
||||
import type { ColumnInfo, ConnectionConfig, ForeignKeyInfo, ObjectInfo, SidebarLayout, TableInfo, TreeNode } from "@/types/database";
|
||||
import { applyPinnedTreeNodeState, updatePinnedTreeNodeInPlace } from "@/lib/pinnedItems";
|
||||
import {
|
||||
reconcileLayout,
|
||||
@@ -51,6 +51,7 @@ import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { encodeSqlServerLinkedSchema, parseSqlServerLinkedSchema } from "@/lib/sqlServerLinkedServers";
|
||||
import { inferMongoCompletionFields, type MongoCompletionField } from "@/lib/mongoCompletion";
|
||||
import { completionSchemasFromTree, completionTablesFromTree } from "@/lib/completionTreeIndex";
|
||||
|
||||
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
|
||||
const ACTIVE_CONNECTION_STORAGE_KEY = "dbx-active-connection";
|
||||
@@ -338,6 +339,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
h2: "H2",
|
||||
snowflake: "Snowflake",
|
||||
trino: "Trino",
|
||||
prestosql: "PrestoSQL",
|
||||
hive: "Hive",
|
||||
db2: "DB2",
|
||||
informix: "Informix",
|
||||
@@ -520,6 +522,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return refreshedGroup?.children ?? [];
|
||||
}
|
||||
|
||||
function tableInfosToCompletionTables(tables: readonly TableInfo[], schema?: string): SqlCompletionTable[] {
|
||||
return tables.map((table) => ({
|
||||
name: table.name,
|
||||
schema,
|
||||
type: table.table_type === "VIEW" || table.table_type === "MATERIALIZED VIEW" ? "view" : "table",
|
||||
}));
|
||||
}
|
||||
|
||||
async function loadPagedTableGroupChildren(options: {
|
||||
node: TreeNode;
|
||||
parentNodeId: string;
|
||||
@@ -539,6 +549,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const tables = await api.listTables(options.node.connectionId, options.node.database, options.querySchema, searchFilter, fetchLimit, searchFilter ? undefined : options.offset, options.objectTypes);
|
||||
const hasMore = searchFilter ? false : tables.length > options.pageSize;
|
||||
const pageTables = hasMore ? tables.slice(0, options.pageSize) : tables;
|
||||
indexCompletionTables(options.node.connectionId, options.node.database, options.effectiveSchema, tableInfosToCompletionTables(pageTables, options.effectiveSchema));
|
||||
const objects = mergeTableInfosIntoObjects([], pageTables, options.effectiveSchema);
|
||||
const visibleObjectCount = objects.filter((object) => options.objectTypes.includes(normalizedObjectTreeKind(object.object_type))).length;
|
||||
return {
|
||||
@@ -1533,6 +1544,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (simpleObjectDisplay) {
|
||||
try {
|
||||
const [objects, tables] = await Promise.all([api.listObjects(connectionId, database, querySchema), api.listTables(connectionId, database, querySchema)]);
|
||||
indexCompletionTables(connectionId, database, effectiveSchema, tableInfosToCompletionTables(tables, effectiveSchema));
|
||||
children = buildSimpleObjectTreeNodes({
|
||||
nodeId,
|
||||
connectionId,
|
||||
@@ -1542,6 +1554,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
});
|
||||
} catch {
|
||||
const tables = await api.listTables(connectionId, database, querySchema);
|
||||
indexCompletionTables(connectionId, database, effectiveSchema, tableInfosToCompletionTables(tables, effectiveSchema));
|
||||
children = buildTableTreeNodes({ nodeId, connectionId, database, schema: effectiveSchema, tables });
|
||||
}
|
||||
} else {
|
||||
@@ -2190,8 +2203,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const allScopes = [...completionTableIndex.entries()].filter(([key]) => key.startsWith(`${connectionId}:${database}:`)).map(([, entry]) => entry);
|
||||
const preferred = schema ? completionTableIndex.get(completionScopeKey(connectionId, database, schema)) : undefined;
|
||||
const scopes = preferred ? [preferred, ...allScopes.filter((entry) => entry !== preferred)] : allScopes;
|
||||
const treeTables = completionTablesFromTree(treeNodes.value, connectionId, database, schema);
|
||||
const ranked = scopes
|
||||
.flatMap((entry) => entry?.tables ?? [])
|
||||
.concat(treeTables)
|
||||
.map((table) => ({ table, score: tableMatchScore(table, filter, schema) }))
|
||||
.filter((entry) => entry.score >= 0)
|
||||
.sort((a, b) => b.score - a.score || a.table.name.localeCompare(b.table.name));
|
||||
@@ -2211,7 +2226,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
|
||||
function lookupLocalCompletionSchemas(connectionId: string, database: string, filter = "", limit = 50): string[] {
|
||||
const schemas = schemaListCache.value[`${connectionId}:${database}`] ?? [];
|
||||
const schemas = dedupeCompletionQualifierNames([...(schemaListCache.value[`${connectionId}:${database}`] ?? []), ...completionSchemasFromTree(treeNodes.value, connectionId, database)]);
|
||||
const normalized = filter.trim().toLowerCase();
|
||||
return schemas
|
||||
.filter((schema) => fuzzyTextMatch(schema, normalized))
|
||||
@@ -2228,6 +2243,20 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function dedupeCompletionQualifierNames(names: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const name of names) {
|
||||
const normalized = name.trim();
|
||||
if (!normalized) continue;
|
||||
const key = normalized.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(normalized);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function lookupLocalCompletionColumns(connectionId: string, database: string, table: string, schema?: string): SqlCompletionColumn[] {
|
||||
return completionColumnIndex.get(completionColumnsKey(connectionId, database, table, schema))?.columns ?? [];
|
||||
}
|
||||
@@ -2404,7 +2433,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
const limitedTables = limit ? dedupeCompletionTables(results).slice(0, limit) : results;
|
||||
completionTablesCache.value[cacheKey] = limitedTables;
|
||||
indexCompletionTables(connectionId, database, schema, limitedTables);
|
||||
indexCompletionTables(connectionId, database, undefined, limitedTables);
|
||||
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
|
||||
return completionTablesCache.value[cacheKey];
|
||||
}
|
||||
@@ -2424,7 +2453,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}),
|
||||
);
|
||||
completionTablesCache.value[cacheKey] = tableGroups.flat();
|
||||
indexCompletionTables(connectionId, database, schema, completionTablesCache.value[cacheKey]);
|
||||
indexCompletionTables(connectionId, database, undefined, completionTablesCache.value[cacheKey]);
|
||||
evictOldestCacheEntries(completionTablesCache.value, COMPLETION_CACHE_MAX);
|
||||
return completionTablesCache.value[cacheKey];
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export type DatabaseType =
|
||||
| "h2"
|
||||
| "snowflake"
|
||||
| "trino"
|
||||
| "prestosql"
|
||||
| "hive"
|
||||
| "db2"
|
||||
| "informix"
|
||||
|
||||
@@ -1163,7 +1163,7 @@
|
||||
},
|
||||
{
|
||||
"dbType": "trino",
|
||||
"label": "Trino (Presto)",
|
||||
"label": "Trino",
|
||||
"runtimeMode": "agent",
|
||||
"mcpMode": "bridge",
|
||||
"agentKey": "trino",
|
||||
@@ -1191,6 +1191,35 @@
|
||||
"driverManagement": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"dbType": "prestosql",
|
||||
"label": "PrestoSQL",
|
||||
"runtimeMode": "external",
|
||||
"mcpMode": "bridge",
|
||||
"singleConnectionPool": false,
|
||||
"metadataConnectionScoped": false,
|
||||
"skipTcpProbe": false,
|
||||
"defaultPort": 8080,
|
||||
"supportLevel": "operate",
|
||||
"capabilities": {
|
||||
"queryExecution": true,
|
||||
"metadataBrowse": true,
|
||||
"objectBrowser": true,
|
||||
"objectSource": false,
|
||||
"schemaSearch": true,
|
||||
"diagram": false,
|
||||
"tableDataEdit": true,
|
||||
"tableStructureEdit": false,
|
||||
"tableImport": false,
|
||||
"dataTransfer": false,
|
||||
"sqlFileExecution": true,
|
||||
"databaseCreate": false,
|
||||
"fieldLineage": false,
|
||||
"sqlExplain": false,
|
||||
"userAdmin": false,
|
||||
"driverManagement": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"dbType": "hive",
|
||||
"label": "Apache Hive",
|
||||
|
||||
@@ -158,7 +158,7 @@ const AGENT_CATALOG: &[AgentCatalogEntry] = &[
|
||||
AgentCatalogEntry {
|
||||
db_type: DatabaseType::Trino,
|
||||
key: "trino",
|
||||
label: "Trino (Presto)",
|
||||
label: "Trino",
|
||||
store_visible: true,
|
||||
profiles: &[],
|
||||
},
|
||||
|
||||
@@ -26,8 +26,8 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da
|
||||
postgres_like_agent_jdbc_connection_string(config, host, port, database)
|
||||
} else if config.db_type == DatabaseType::SapHana {
|
||||
sap_hana_jdbc_connection_string(config, host, port, database)
|
||||
} else if config.db_type == DatabaseType::Trino {
|
||||
trino_agent_jdbc_connection_string(config, host, port, database)
|
||||
} else if matches!(config.db_type, DatabaseType::Trino | DatabaseType::PrestoSql) {
|
||||
trino_like_jdbc_connection_string(config, host, port, database)
|
||||
} else if config.db_type == DatabaseType::H2 {
|
||||
h2_agent_jdbc_connection_string(config)
|
||||
} else {
|
||||
@@ -455,12 +455,17 @@ fn sap_hana_jdbc_connection_string(config: &ConnectionConfig, host: &str, port:
|
||||
}
|
||||
}
|
||||
|
||||
fn trino_agent_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String {
|
||||
pub fn trino_like_jdbc_connection_string(config: &ConnectionConfig, host: &str, port: u16, database: &str) -> String {
|
||||
let jdbc_scheme = match config.db_type {
|
||||
DatabaseType::PrestoSql => "presto",
|
||||
_ => "trino",
|
||||
};
|
||||
let jdbc_prefix = format!("jdbc:{jdbc_scheme}:");
|
||||
let base = config
|
||||
.connection_string
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| value.get(..11).is_some_and(|prefix| prefix.eq_ignore_ascii_case("jdbc:trino:")))
|
||||
.filter(|value| value.get(..jdbc_prefix.len()).is_some_and(|prefix| prefix.eq_ignore_ascii_case(&jdbc_prefix)))
|
||||
.map(|connection_string| {
|
||||
if host == config.host && port == config.port {
|
||||
connection_string.to_string()
|
||||
@@ -471,9 +476,9 @@ fn trino_agent_jdbc_connection_string(config: &ConnectionConfig, host: &str, por
|
||||
.unwrap_or_else(|| {
|
||||
let database = database.trim();
|
||||
if database.is_empty() {
|
||||
format!("jdbc:trino://{host}:{port}")
|
||||
format!("jdbc:{jdbc_scheme}://{host}:{port}")
|
||||
} else {
|
||||
format!("jdbc:trino://{host}:{port}/{database}")
|
||||
format!("jdbc:{jdbc_scheme}://{host}:{port}/{database}")
|
||||
}
|
||||
});
|
||||
|
||||
@@ -906,6 +911,30 @@ mod tests {
|
||||
assert_eq!(params["ssl"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prestosql_jdbc_url_uses_presto_jdbc_scheme() {
|
||||
let mut cfg = config(DatabaseType::PrestoSql, Some("hive/default"));
|
||||
cfg.host = "presto.example.com".to_string();
|
||||
cfg.port = 9090;
|
||||
|
||||
let params = agent_connect_params(&cfg, "presto.example.com", 9090, "hive/default");
|
||||
|
||||
assert_eq!(params["connection_string"], "jdbc:presto://presto.example.com:9090/hive/default");
|
||||
assert_eq!(params["ssl"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prestosql_custom_jdbc_url_rewrites_forwarded_host() {
|
||||
let mut cfg = config(DatabaseType::PrestoSql, Some("hive/default"));
|
||||
cfg.host = "presto.internal".to_string();
|
||||
cfg.port = 9090;
|
||||
cfg.connection_string = Some("jdbc:presto://presto.internal:9090/hive/default?source=dbx".to_string());
|
||||
|
||||
let params = agent_connect_params(&cfg, "127.0.0.1", 19090, "hive/default");
|
||||
|
||||
assert_eq!(params["connection_string"], "jdbc:presto://127.0.0.1:19090/hive/default?source=dbx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trino_agent_url_appends_ssl_when_enabled() {
|
||||
let mut cfg = config(DatabaseType::Trino, Some("hive"));
|
||||
|
||||
@@ -164,3 +164,18 @@ async fn spawn_client_for_key(manager: &AgentManager, key: &str) -> Result<Agent
|
||||
client.try_optional_handshake(manager.agent_app_version()).await;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prestosql_does_not_use_agent_driver() {
|
||||
assert_eq!(runtime_agent_key_candidates(&DatabaseType::PrestoSql, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trino_uses_only_trino_agent_driver() {
|
||||
assert_eq!(runtime_agent_key_candidates(&DatabaseType::Trino, None).unwrap(), vec!["trino"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ pub fn build_agent_list(am: &AgentManager, registry: Option<&AgentRegistry>) ->
|
||||
let installed = am.is_driver_installed(key);
|
||||
let requires_java_runtime = am.driver_requires_java_runtime(key);
|
||||
let local = local_state.installed_drivers.get(key);
|
||||
let remote = registry.and_then(|r| r.drivers.get(key));
|
||||
let remote = registry.and_then(|r| agent_registry_driver(r, key));
|
||||
let jre_key = remote
|
||||
.map(|r| r.jre.clone())
|
||||
.or_else(|| local.map(|l| l.jre.clone()))
|
||||
@@ -485,7 +485,7 @@ async fn install_agent_driver_from_registry(
|
||||
current: Option<u32>,
|
||||
total_drivers: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
let Some(driver) = registry.drivers.get(db_type) else {
|
||||
let Some(driver) = agent_registry_driver(registry, db_type) else {
|
||||
if let Some(local_jar) = find_local_agent_jar(db_type) {
|
||||
install_local_agent(am, db_type, local_jar)?;
|
||||
am.stop_daemon_by_key(db_type).await;
|
||||
@@ -593,6 +593,13 @@ async fn install_agent_driver_from_registry(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn agent_registry_driver<'a>(
|
||||
registry: &'a AgentRegistry,
|
||||
db_type: &str,
|
||||
) -> Option<&'a crate::agent_manager::DriverInfo> {
|
||||
registry.drivers.get(db_type)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn download_with_progress(
|
||||
am: &AgentManager,
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::agent_connection::{
|
||||
agent_connect_params, h2_file_path_from_jdbc_url, is_h2_file_connection, mongo_legacy_error_with_auth_hint,
|
||||
mongo_uses_legacy_driver, oracle_alternate_connect_config_labels, oracle_alternate_connect_configs,
|
||||
oracle_auth_fallback_profiles, oracle_error_with_driver_hint, should_retry_mongo_with_legacy_driver,
|
||||
should_retry_oracle_with_10g_driver,
|
||||
should_retry_oracle_with_10g_driver, trino_like_jdbc_connection_string,
|
||||
};
|
||||
use crate::agent_manager::{JavaRuntimeMode, DEFAULT_JRE_KEY};
|
||||
use crate::database_capabilities;
|
||||
@@ -30,6 +30,7 @@ use crate::storage::Storage;
|
||||
|
||||
pub const JDBC_PLUGIN_NOT_INSTALLED: &str =
|
||||
"JDBC plugin is not installed. Install the optional JDBC plugin to use this connection.";
|
||||
pub const PRESTOSQL_JDBC_DRIVER_CLASS: &str = "io.prestosql.jdbc.PrestoDriver";
|
||||
const DEFAULT_AGENT_CONNECT_TIMEOUT_SECS: u64 = 30;
|
||||
const ACCESS_AGENT_CONNECT_TIMEOUT_SECS: u64 = 30;
|
||||
const POOL_CLOSE_TIMEOUT_SECS: u64 = 5;
|
||||
@@ -81,6 +82,44 @@ pub enum PoolKind {
|
||||
MessageQueue,
|
||||
}
|
||||
|
||||
macro_rules! agent_connection_pool_database_type {
|
||||
() => {
|
||||
DatabaseType::Dameng
|
||||
| DatabaseType::Kingbase
|
||||
| DatabaseType::Highgo
|
||||
| DatabaseType::Vastbase
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Databend
|
||||
| DatabaseType::Yashandb
|
||||
| DatabaseType::Databricks
|
||||
| DatabaseType::SapHana
|
||||
| DatabaseType::Teradata
|
||||
| DatabaseType::Vertica
|
||||
| DatabaseType::Firebird
|
||||
| DatabaseType::Exasol
|
||||
| DatabaseType::OceanbaseOracle
|
||||
| DatabaseType::Gbase
|
||||
| DatabaseType::Oracle
|
||||
| DatabaseType::H2
|
||||
| DatabaseType::Snowflake
|
||||
| DatabaseType::Trino
|
||||
| DatabaseType::Hive
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Informix
|
||||
| DatabaseType::Neo4j
|
||||
| DatabaseType::Cassandra
|
||||
| DatabaseType::Bigquery
|
||||
| DatabaseType::Kylin
|
||||
| DatabaseType::Sundb
|
||||
| DatabaseType::Tdengine
|
||||
| DatabaseType::Xugu
|
||||
| DatabaseType::Iotdb
|
||||
| DatabaseType::Etcd
|
||||
| DatabaseType::Iris
|
||||
| DatabaseType::Access
|
||||
};
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
pub connections: Arc<RwLock<HashMap<String, PoolKind>>>,
|
||||
keepalive_tasks: Arc<RwLock<HashMap<String, JoinHandle<()>>>>,
|
||||
@@ -151,6 +190,16 @@ pub fn database_connection_config(config: &ConnectionConfig, database: Option<&s
|
||||
db_config
|
||||
}
|
||||
|
||||
pub fn prestosql_jdbc_config_for_endpoint(config: &ConnectionConfig, host: &str, port: u16) -> ConnectionConfig {
|
||||
let mut jdbc_config = config.clone();
|
||||
jdbc_config.connection_string =
|
||||
Some(trino_like_jdbc_connection_string(config, host, port, config.effective_database().unwrap_or("")));
|
||||
if jdbc_config.jdbc_driver_class.as_deref().is_none_or(|value| value.trim().is_empty()) {
|
||||
jdbc_config.jdbc_driver_class = Some(PRESTOSQL_JDBC_DRIVER_CLASS.to_string());
|
||||
}
|
||||
jdbc_config
|
||||
}
|
||||
|
||||
pub async fn connect_mysql_metadata_pool(
|
||||
config: &ConnectionConfig,
|
||||
db_config: &ConnectionConfig,
|
||||
@@ -347,7 +396,7 @@ impl AppState {
|
||||
Ok(PoolKind::ExternalDriver { driver_id: driver_id.to_string(), config: Arc::new(config.clone()), session })
|
||||
}
|
||||
|
||||
fn external_driver_runtime_env(&self, driver_id: &str) -> Result<PluginRuntimeEnv, String> {
|
||||
pub fn external_driver_runtime_env(&self, driver_id: &str) -> Result<PluginRuntimeEnv, String> {
|
||||
if driver_id != "jdbc" {
|
||||
return Ok(PluginRuntimeEnv::default());
|
||||
}
|
||||
@@ -732,39 +781,7 @@ impl AppState {
|
||||
db::influxdb_driver::test_connection(&client, connect_timeout).await?;
|
||||
PoolKind::InfluxDb(client)
|
||||
}
|
||||
DatabaseType::Dameng
|
||||
| DatabaseType::Kingbase
|
||||
| DatabaseType::Highgo
|
||||
| DatabaseType::Vastbase
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Databend
|
||||
| DatabaseType::Yashandb
|
||||
| DatabaseType::Databricks
|
||||
| DatabaseType::SapHana
|
||||
| DatabaseType::Teradata
|
||||
| DatabaseType::Vertica
|
||||
| DatabaseType::Firebird
|
||||
| DatabaseType::Exasol
|
||||
| DatabaseType::OceanbaseOracle
|
||||
| DatabaseType::Gbase
|
||||
| DatabaseType::Oracle
|
||||
| DatabaseType::H2
|
||||
| DatabaseType::Snowflake
|
||||
| DatabaseType::Trino
|
||||
| DatabaseType::Hive
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Informix
|
||||
| DatabaseType::Neo4j
|
||||
| DatabaseType::Cassandra
|
||||
| DatabaseType::Bigquery
|
||||
| DatabaseType::Kylin
|
||||
| DatabaseType::Sundb
|
||||
| DatabaseType::Tdengine
|
||||
| DatabaseType::Xugu
|
||||
| DatabaseType::Iotdb
|
||||
| DatabaseType::Etcd
|
||||
| DatabaseType::Iris
|
||||
| DatabaseType::Access => {
|
||||
agent_connection_pool_database_type!() => {
|
||||
let connect_params =
|
||||
agent_connect_params(&db_config, &host, port, db_config.effective_database().unwrap_or(""));
|
||||
let mut client =
|
||||
@@ -865,6 +882,10 @@ impl AppState {
|
||||
}
|
||||
PoolKind::Agent(Arc::new(tokio::sync::Mutex::new(client)))
|
||||
}
|
||||
DatabaseType::PrestoSql => {
|
||||
let jdbc_config = prestosql_jdbc_config_for_endpoint(&db_config, &host, port);
|
||||
self.external_driver_pool("jdbc", &jdbc_config).await?
|
||||
}
|
||||
DatabaseType::Jdbc => {
|
||||
let mut jdbc_config = db_config.clone();
|
||||
if host != config.host || port != config.port {
|
||||
@@ -1986,6 +2007,11 @@ fn shares_database_pool_with_connection(db_type: &DatabaseType) -> bool {
|
||||
matches!(db_type, DatabaseType::Oracle)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn uses_agent_connection_pool(db_type: &DatabaseType) -> bool {
|
||||
matches!(*db_type, agent_connection_pool_database_type!())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn uses_bare_mysql_pool(db_type: &DatabaseType) -> bool {
|
||||
matches!(db_type, DatabaseType::Doris | DatabaseType::StarRocks | DatabaseType::ManticoreSearch)
|
||||
@@ -2177,8 +2203,9 @@ async fn detect_ob_oracle_mode(config: &ConnectionConfig, pool: &db::mysql::MySq
|
||||
mod tests {
|
||||
use super::{
|
||||
agent_connect_timeout, connection_remote_endpoint, connection_url_for_endpoint, database_connection_config,
|
||||
metadata_connection_config, mysql_metadata_fallback_url, redacted_connection_url_for_endpoint,
|
||||
uses_bare_mysql_pool, uses_tcp_probe, validate_h2_database_path, AppState, PoolKind,
|
||||
metadata_connection_config, mysql_metadata_fallback_url, prestosql_jdbc_config_for_endpoint,
|
||||
redacted_connection_url_for_endpoint, uses_bare_mysql_pool, uses_tcp_probe, validate_h2_database_path,
|
||||
AppState, PoolKind, PRESTOSQL_JDBC_DRIVER_CLASS,
|
||||
};
|
||||
use crate::agent_connection::{
|
||||
agent_connect_params, mongo_legacy_error_with_auth_hint, mongo_uses_legacy_driver,
|
||||
@@ -2264,6 +2291,32 @@ mod tests {
|
||||
assert_eq!(agent_connect_timeout(&config).as_secs(), 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prestosql_jdbc_config_sets_presto_url_and_driver_class() {
|
||||
let mut config = mysql_config(Some("hive/default"));
|
||||
config.db_type = DatabaseType::PrestoSql;
|
||||
config.host = "presto.example.com".to_string();
|
||||
config.port = 9090;
|
||||
|
||||
let jdbc_config = prestosql_jdbc_config_for_endpoint(&config, "127.0.0.1", 19090);
|
||||
|
||||
assert_eq!(jdbc_config.connection_string.as_deref(), Some("jdbc:presto://127.0.0.1:19090/hive/default"));
|
||||
assert_eq!(jdbc_config.jdbc_driver_class.as_deref(), Some(PRESTOSQL_JDBC_DRIVER_CLASS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prestosql_jdbc_config_preserves_custom_driver_class_and_paths() {
|
||||
let mut config = mysql_config(Some("hive"));
|
||||
config.db_type = DatabaseType::PrestoSql;
|
||||
config.jdbc_driver_class = Some("custom.PrestoDriver".to_string());
|
||||
config.jdbc_driver_paths = vec!["D:\\software\\jar\\presto-jdbc-350.jar".to_string()];
|
||||
|
||||
let jdbc_config = prestosql_jdbc_config_for_endpoint(&config, "presto.example.com", 9090);
|
||||
|
||||
assert_eq!(jdbc_config.jdbc_driver_class.as_deref(), Some("custom.PrestoDriver"));
|
||||
assert_eq!(jdbc_config.jdbc_driver_paths, vec!["D:\\software\\jar\\presto-jdbc-350.jar"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_connect_params_include_url_params() {
|
||||
let mut config = mysql_config(Some("testdb"));
|
||||
@@ -2907,7 +2960,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_direct_connections_skip_tcp_probe() {
|
||||
fn prestosql_uses_external_driver_pool_not_agent_pool() {
|
||||
assert!(!super::uses_agent_connection_pool(&DatabaseType::PrestoSql));
|
||||
assert!(super::uses_agent_connection_pool(&DatabaseType::Trino));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_hostname_connections_skip_tcp_probe() {
|
||||
let mut config = mysql_config(Some("app"));
|
||||
config.host = "mysql.example.com".to_string();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::agent_service::AgentProgressEvent;
|
||||
use crate::plugins::PluginRuntimeEnv;
|
||||
use crate::plugins::{PluginManifest, SUPPORTED_PLUGIN_PROTOCOL_VERSION};
|
||||
use crate::update::{fetch_latest_release, is_newer_version, JdbcPluginLatest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -9,6 +10,11 @@ use tokio::process::Command;
|
||||
const JDBC_PLUGIN_DOWNLOAD_URL: &str =
|
||||
"https://github.com/t8y2/dbx/releases/latest/download/dbx-jdbc-plugin-latest.zip";
|
||||
const JDBC_PLUGIN_R2_PATH: &str = "releases/latest/dbx-jdbc-plugin-latest.zip";
|
||||
pub const PRESTOSQL_JDBC_DRIVER_VERSION: &str = "350";
|
||||
pub const PRESTOSQL_JDBC_DRIVER_COORDINATE: &str = "io.prestosql:presto-jdbc:350";
|
||||
pub const PRESTOSQL_JDBC_DRIVER_REPOSITORY: &str = "https://repo.maven.apache.org/maven2/";
|
||||
const PRESTOSQL_JDBC_DRIVER_URL: &str =
|
||||
"https://repo.maven.apache.org/maven2/io/prestosql/presto-jdbc/350/presto-jdbc-350.jar";
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JdbcDriverInfo {
|
||||
@@ -124,6 +130,7 @@ pub fn delete_jdbc_maven_bundle(plugins_root: &Path, bundle_id: &str) -> Result<
|
||||
pub async fn install_jdbc_driver_from_maven(
|
||||
plugins_root: &Path,
|
||||
request: JdbcMavenInstallRequest,
|
||||
env: PluginRuntimeEnv,
|
||||
) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
let coordinate = request.coordinate.trim().to_string();
|
||||
if coordinate.is_empty() {
|
||||
@@ -147,6 +154,7 @@ pub async fn install_jdbc_driver_from_maven(
|
||||
std::fs::create_dir_all(&local_repo).map_err(|err| err.to_string())?;
|
||||
|
||||
let mut command = Command::new(&resolver);
|
||||
env.apply_to(&mut command);
|
||||
command.arg("resolve").arg("--coordinate").arg(&coordinate).arg("--local-repo").arg(&local_repo);
|
||||
for repo in &repositories {
|
||||
command.arg("--repo").arg(repo);
|
||||
@@ -166,6 +174,51 @@ pub async fn install_jdbc_driver_from_maven(
|
||||
list_jdbc_drivers(plugins_root)
|
||||
}
|
||||
|
||||
pub async fn install_prestosql_jdbc_driver(plugins_root: &Path) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
let plugin_dir = plugins_root.join("jdbc");
|
||||
let temp_dir = plugin_dir.join("downloads");
|
||||
std::fs::create_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
||||
let temp_file = temp_dir.join("presto-jdbc-350.jar.tmp");
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()
|
||||
.map_err(|err| err.to_string())?;
|
||||
let mut resp = client
|
||||
.get(PRESTOSQL_JDBC_DRIVER_URL)
|
||||
.header(reqwest::header::USER_AGENT, "dbx-prestosql-driver-installer")
|
||||
.send()
|
||||
.await
|
||||
.and_then(|resp| resp.error_for_status())
|
||||
.map_err(|err| format!("Failed to download PrestoSQL JDBC driver: {err}"))?;
|
||||
let mut file = std::fs::File::create(&temp_file)
|
||||
.map_err(|err| format!("Failed to create PrestoSQL JDBC driver temp file: {err}"))?;
|
||||
while let Some(chunk) = resp.chunk().await.map_err(|err| format!("Download stream error: {err}"))? {
|
||||
std::io::Write::write_all(&mut file, &chunk).map_err(|err| format!("Failed to write chunk: {err}"))?;
|
||||
}
|
||||
std::io::Write::flush(&mut file).map_err(|err| format!("Failed to flush temp file: {err}"))?;
|
||||
drop(file);
|
||||
|
||||
let root = plugins_root.to_path_buf();
|
||||
let repositories = vec![PRESTOSQL_JDBC_DRIVER_REPOSITORY.to_string()];
|
||||
tokio::task::spawn_blocking(move || {
|
||||
install_single_jdbc_maven_artifact(
|
||||
&root,
|
||||
PRESTOSQL_JDBC_DRIVER_COORDINATE,
|
||||
"io.prestosql",
|
||||
"presto-jdbc",
|
||||
PRESTOSQL_JDBC_DRIVER_VERSION,
|
||||
&temp_file,
|
||||
&repositories,
|
||||
)?;
|
||||
let _ = std::fs::remove_file(&temp_file);
|
||||
Ok::<(), String>(())
|
||||
})
|
||||
.await
|
||||
.map_err(|err| err.to_string())??;
|
||||
list_jdbc_drivers(plugins_root)
|
||||
}
|
||||
|
||||
// ---- JDBC Plugin ----
|
||||
|
||||
pub async fn get_jdbc_plugin_status(plugins_root: &Path) -> Result<JdbcPluginStatus, String> {
|
||||
@@ -595,6 +648,63 @@ fn install_jdbc_maven_bundle(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_single_jdbc_maven_artifact(
|
||||
plugins_root: &Path,
|
||||
coordinate: &str,
|
||||
group_id: &str,
|
||||
artifact_id: &str,
|
||||
version: &str,
|
||||
source: &Path,
|
||||
repositories: &[String],
|
||||
) -> Result<(), String> {
|
||||
if !source.exists() {
|
||||
return Err(format!("Driver JAR does not exist: {}", source.display()));
|
||||
}
|
||||
let bundle_id = maven_bundle_id(coordinate);
|
||||
let bundle_dir = jdbc_maven_drivers_dir(plugins_root).join(&bundle_id);
|
||||
let temp_dir = bundle_dir.with_extension("tmp");
|
||||
if temp_dir.exists() {
|
||||
std::fs::remove_dir_all(&temp_dir).map_err(|err| err.to_string())?;
|
||||
}
|
||||
std::fs::create_dir_all(temp_dir.join("jars")).map_err(|err| err.to_string())?;
|
||||
|
||||
let file_name = source
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| format!("Invalid driver file path: {}", source.display()))?;
|
||||
let target = temp_dir.join("jars").join(file_name);
|
||||
std::fs::copy(source, &target)
|
||||
.map_err(|err| format!("Failed to copy {} to {}: {err}", source.display(), target.display()))?;
|
||||
let metadata = std::fs::metadata(&target).map_err(|err| err.to_string())?;
|
||||
let final_path = bundle_dir.join("jars").join(file_name);
|
||||
let manifest = JdbcMavenBundleInfo {
|
||||
id: bundle_id.clone(),
|
||||
coordinate: coordinate.to_string(),
|
||||
scope: "runtime".to_string(),
|
||||
repositories: repositories.to_vec(),
|
||||
installed_at: chrono::Utc::now().to_rfc3339(),
|
||||
path: bundle_dir.to_string_lossy().to_string(),
|
||||
artifacts: vec![JdbcMavenArtifactInfo {
|
||||
group_id: group_id.to_string(),
|
||||
artifact_id: artifact_id.to_string(),
|
||||
version: version.to_string(),
|
||||
classifier: String::new(),
|
||||
extension: "jar".to_string(),
|
||||
file_name: file_name.to_string(),
|
||||
path: final_path.to_string_lossy().to_string(),
|
||||
size: metadata.len(),
|
||||
sha256: file_sha256(&target)?,
|
||||
}],
|
||||
};
|
||||
let manifest_json = serde_json::to_string_pretty(&manifest).map_err(|err| err.to_string())?;
|
||||
std::fs::write(temp_dir.join("manifest.json"), manifest_json).map_err(|err| err.to_string())?;
|
||||
if bundle_dir.exists() {
|
||||
std::fs::remove_dir_all(&bundle_dir).map_err(|err| err.to_string())?;
|
||||
}
|
||||
std::fs::rename(&temp_dir, &bundle_dir).map_err(|err| err.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn maven_bundle_id(coordinate: &str) -> String {
|
||||
let mut id = String::with_capacity(coordinate.len());
|
||||
for ch in coordinate.trim().chars() {
|
||||
@@ -661,6 +771,7 @@ pub fn unique_target_path(dir: &Path, file_name: &str) -> PathBuf {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::plugins::PluginRuntimeEnv;
|
||||
|
||||
#[test]
|
||||
fn maven_bundle_install_lists_nested_jars() {
|
||||
@@ -706,4 +817,100 @@ mod tests {
|
||||
assert!(is_safe_bundle_id("org.apache.hive_hive-jdbc_4.0.1_standalone"));
|
||||
assert!(!is_safe_bundle_id("../hive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installs_prestosql_direct_driver_as_builtin_bundle() {
|
||||
let root = std::env::temp_dir().join(format!("dbx-prestosql-direct-driver-test-{}", uuid::Uuid::new_v4()));
|
||||
let source_dir = root.join("source");
|
||||
std::fs::create_dir_all(&source_dir).unwrap();
|
||||
let source = source_dir.join("presto-jdbc-350.jar");
|
||||
std::fs::write(&source, b"presto").unwrap();
|
||||
|
||||
install_single_jdbc_maven_artifact(
|
||||
&root,
|
||||
PRESTOSQL_JDBC_DRIVER_COORDINATE,
|
||||
"io.prestosql",
|
||||
"presto-jdbc",
|
||||
PRESTOSQL_JDBC_DRIVER_VERSION,
|
||||
&source,
|
||||
&[PRESTOSQL_JDBC_DRIVER_REPOSITORY.to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let bundles = list_jdbc_maven_bundles(&root).unwrap();
|
||||
assert_eq!(bundles.len(), 1);
|
||||
assert_eq!(bundles[0].id, "io.prestosql_presto-jdbc_350");
|
||||
assert_eq!(bundles[0].coordinate, PRESTOSQL_JDBC_DRIVER_COORDINATE);
|
||||
assert_eq!(bundles[0].artifacts[0].file_name, "presto-jdbc-350.jar");
|
||||
|
||||
let drivers = list_jdbc_drivers(&root).unwrap();
|
||||
assert_eq!(drivers.len(), 1);
|
||||
assert_eq!(drivers[0].bundle_id.as_deref(), Some("io.prestosql_presto-jdbc_350"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn maven_resolver_receives_runtime_env() {
|
||||
let root = std::env::temp_dir().join(format!("dbx-jdbc-maven-env-test-{}", uuid::Uuid::new_v4()));
|
||||
let plugin_dir = root.join("jdbc");
|
||||
let bin_dir = plugin_dir.join("bin");
|
||||
let source_dir = root.join("source");
|
||||
std::fs::create_dir_all(&bin_dir).unwrap();
|
||||
std::fs::create_dir_all(&source_dir).unwrap();
|
||||
let source = source_dir.join("env-driver.jar");
|
||||
std::fs::write(&source, b"jar").unwrap();
|
||||
|
||||
let output = serde_json::json!({
|
||||
"coordinate": "com.example:env-driver:1.0.0",
|
||||
"scope": "runtime",
|
||||
"repositories": ["https://repo.maven.apache.org/maven2/"],
|
||||
"artifacts": [{
|
||||
"groupId": "com.example",
|
||||
"artifactId": "env-driver",
|
||||
"version": "1.0.0",
|
||||
"classifier": "",
|
||||
"extension": "jar",
|
||||
"file": source.to_string_lossy(),
|
||||
}],
|
||||
})
|
||||
.to_string();
|
||||
let resolver = jdbc_maven_resolver_executable(&plugin_dir);
|
||||
if cfg!(windows) {
|
||||
std::fs::write(
|
||||
&resolver,
|
||||
format!(
|
||||
"@echo off\r\nif \"%DBX_JAVA_BIN%\"==\"\" (\r\n echo missing DBX_JAVA_BIN 1>&2\r\n exit /b 2\r\n)\r\necho {output}\r\n"
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
std::fs::write(
|
||||
&resolver,
|
||||
format!(
|
||||
"#!/usr/bin/env sh\nif [ -z \"${{DBX_JAVA_BIN:-}}\" ]; then\n echo missing DBX_JAVA_BIN >&2\n exit 2\nfi\necho '{}'\n",
|
||||
output.replace('\'', "'\\''")
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&resolver, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let drivers = install_jdbc_driver_from_maven(
|
||||
&root,
|
||||
JdbcMavenInstallRequest { coordinate: "com.example:env-driver:1.0.0".to_string(), repositories: vec![] },
|
||||
PluginRuntimeEnv::default().with_var("DBX_JAVA_BIN", "java-from-managed-jre"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(drivers.len(), 1);
|
||||
assert_eq!(drivers[0].name, "env-driver.jar");
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +290,8 @@ pub enum DatabaseType {
|
||||
H2,
|
||||
Snowflake,
|
||||
Trino,
|
||||
#[serde(rename = "prestosql")]
|
||||
PrestoSql,
|
||||
Hive,
|
||||
#[serde(rename = "db2")]
|
||||
Db2,
|
||||
@@ -762,6 +764,7 @@ impl ConnectionConfig {
|
||||
DatabaseType::H2 => format!("h2://{host}:{port}{db_part}"),
|
||||
DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"),
|
||||
DatabaseType::Trino => format!("trino://{host}:{port}{db_part}"),
|
||||
DatabaseType::PrestoSql => format!("prestosql://{host}:{port}{db_part}"),
|
||||
DatabaseType::Hive => format!("hive://{host}:{port}{db_part}"),
|
||||
DatabaseType::Db2 => format!("db2://{host}:{port}{db_part}"),
|
||||
DatabaseType::Informix => format!("informix://{host}:{port}{db_part}"),
|
||||
@@ -936,6 +939,9 @@ impl ConnectionConfig {
|
||||
DatabaseType::Trino => {
|
||||
format!("trino://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::PrestoSql => {
|
||||
format!("prestosql://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
DatabaseType::Hive => {
|
||||
format!("hive://{}:{}@{host}:{port}{db_part}", username, password)
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl PluginRuntimeEnv {
|
||||
self.vars.iter().find_map(|(name, value)| (name == key).then_some(value.as_str()))
|
||||
}
|
||||
|
||||
fn apply_to(&self, command: &mut Command) {
|
||||
pub fn apply_to(&self, command: &mut Command) {
|
||||
for (key, value) in &self.vars {
|
||||
command.env(key, value);
|
||||
}
|
||||
|
||||
@@ -527,6 +527,11 @@ async fn list_tables_once(
|
||||
let config = config.clone();
|
||||
let session = session.clone();
|
||||
drop(connections);
|
||||
if uses_presto_like_information_schema_tables(&config.db_type) {
|
||||
return external_driver_presto_like_tables(session, config.as_ref(), database, schema)
|
||||
.await
|
||||
.map(|tables| filter_table_infos(tables, filter, limit, offset, object_types));
|
||||
}
|
||||
return session
|
||||
.invoke::<Vec<db::TableInfo>>(
|
||||
"listTables",
|
||||
@@ -750,11 +755,123 @@ fn normalize_table_info_object_type(value: &str) -> String {
|
||||
"TABLE".to_string()
|
||||
}
|
||||
|
||||
fn uses_presto_like_information_schema_tables(db_type: &DatabaseType) -> bool {
|
||||
matches!(db_type, DatabaseType::PrestoSql | DatabaseType::Trino)
|
||||
}
|
||||
|
||||
async fn external_driver_presto_like_tables(
|
||||
session: Arc<crate::plugins::PluginDriverSession>,
|
||||
config: &ConnectionConfig,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
) -> Result<Vec<db::TableInfo>, String> {
|
||||
let result: db::QueryResult = session
|
||||
.invoke(
|
||||
"executeQuery",
|
||||
serde_json::json!({
|
||||
"connection": config,
|
||||
"database": database,
|
||||
"schema": schema,
|
||||
"sql": presto_like_information_schema_tables_sql(database, schema),
|
||||
"maxRows": 100000,
|
||||
"fetchSize": 1000,
|
||||
"timeoutSecs": 60
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
Ok(presto_like_tables_from_query_result(&result))
|
||||
}
|
||||
|
||||
async fn external_driver_presto_like_objects(
|
||||
session: Arc<crate::plugins::PluginDriverSession>,
|
||||
config: &ConnectionConfig,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
) -> Result<Vec<db::ObjectInfo>, String> {
|
||||
let tables = external_driver_presto_like_tables(session, config, database, schema).await?;
|
||||
Ok(tables
|
||||
.into_iter()
|
||||
.map(|table| db::ObjectInfo {
|
||||
name: table.name,
|
||||
object_type: table.table_type,
|
||||
schema: Some(schema.to_string()),
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
parent_schema: table.parent_schema,
|
||||
parent_name: table.parent_name,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn presto_like_information_schema_tables_sql(database: &str, schema: &str) -> String {
|
||||
let source = if database.trim().is_empty() {
|
||||
"information_schema.tables".to_string()
|
||||
} else {
|
||||
format!("{}.information_schema.tables", quote_presto_like_identifier(database))
|
||||
};
|
||||
format!(
|
||||
"SELECT table_name, CASE table_type WHEN 'BASE TABLE' THEN 'TABLE' ELSE table_type END AS table_type \
|
||||
FROM {source} \
|
||||
WHERE table_schema = {} AND table_type IN ('BASE TABLE', 'VIEW') \
|
||||
ORDER BY table_type, table_name",
|
||||
sql_string_literal(schema)
|
||||
)
|
||||
}
|
||||
|
||||
fn presto_like_tables_from_query_result(result: &db::QueryResult) -> Vec<db::TableInfo> {
|
||||
result
|
||||
.rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
let name = query_result_cell_string(row, 0)?;
|
||||
if name.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(db::TableInfo {
|
||||
name,
|
||||
table_type: normalize_information_schema_table_type(
|
||||
query_result_cell_string(row, 1).as_deref().unwrap_or("TABLE"),
|
||||
),
|
||||
comment: None,
|
||||
parent_schema: None,
|
||||
parent_name: None,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn query_result_cell_string(row: &[serde_json::Value], index: usize) -> Option<String> {
|
||||
let value = row.get(index)?;
|
||||
if value.is_null() {
|
||||
return None;
|
||||
}
|
||||
value.as_str().map(ToString::to_string).or_else(|| Some(value.to_string()))
|
||||
}
|
||||
|
||||
fn normalize_information_schema_table_type(table_type: &str) -> String {
|
||||
match table_type.trim().to_ascii_uppercase().replace(' ', "_").as_str() {
|
||||
"BASE_TABLE" => "TABLE".to_string(),
|
||||
"VIEW" => "VIEW".to_string(),
|
||||
"MATERIALIZED_VIEW" => "MATERIALIZED_VIEW".to_string(),
|
||||
_ => table_type.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn quote_presto_like_identifier(identifier: &str) -> String {
|
||||
format!("\"{}\"", identifier.replace('"', "\"\""))
|
||||
}
|
||||
|
||||
fn sql_string_literal(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
clickhouse_metadata_database, deduplicate_column_infos, filter_mysql_system_databases_for_config,
|
||||
filter_table_infos, is_agent_postgres_metadata_fallback_config,
|
||||
filter_table_infos, is_agent_postgres_metadata_fallback_config, normalize_information_schema_table_type,
|
||||
presto_like_information_schema_tables_sql, presto_like_tables_from_query_result,
|
||||
};
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
use super::{duckdb_attach_database, duckdb_list_databases, duckdb_query_tables_in_database};
|
||||
@@ -922,6 +1039,51 @@ mod tests {
|
||||
assert_eq!(filtered[0].name, "active_users");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presto_like_information_schema_sql_uses_catalog_and_schema_without_system_jdbc() {
|
||||
let sql = presto_like_information_schema_tables_sql("hive", "sales_analytics");
|
||||
|
||||
assert_eq!(
|
||||
sql,
|
||||
"SELECT table_name, CASE table_type WHEN 'BASE TABLE' THEN 'TABLE' ELSE table_type END AS table_type FROM \"hive\".information_schema.tables WHERE table_schema = 'sales_analytics' AND table_type IN ('BASE TABLE', 'VIEW') ORDER BY table_type, table_name"
|
||||
);
|
||||
assert!(!sql.contains("system.jdbc.tables"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presto_like_information_schema_sql_escapes_identifiers_and_literals() {
|
||||
let sql = presto_like_information_schema_tables_sql("hi\"ve", "sales'analytics");
|
||||
|
||||
assert!(sql.contains("\"hi\"\"ve\".information_schema.tables"));
|
||||
assert!(sql.contains("table_schema = 'sales''analytics'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presto_like_tables_from_query_result_normalizes_base_table_type() {
|
||||
let result = super::db::QueryResult {
|
||||
columns: vec!["table_name".to_string(), "table_type".to_string()],
|
||||
column_types: vec![],
|
||||
column_sortables: vec![],
|
||||
rows: vec![
|
||||
vec![serde_json::json!("daily_revenue"), serde_json::json!("BASE TABLE")],
|
||||
vec![serde_json::json!("revenue_view"), serde_json::json!("VIEW")],
|
||||
],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 1,
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
};
|
||||
|
||||
let tables = presto_like_tables_from_query_result(&result);
|
||||
|
||||
assert_eq!(tables[0].name, "daily_revenue");
|
||||
assert_eq!(tables[0].table_type, "TABLE");
|
||||
assert_eq!(tables[1].name, "revenue_view");
|
||||
assert_eq!(tables[1].table_type, "VIEW");
|
||||
assert_eq!(normalize_information_schema_table_type("MATERIALIZED VIEW"), "MATERIALIZED_VIEW");
|
||||
}
|
||||
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
#[test]
|
||||
fn duckdb_list_databases_includes_attached_database() {
|
||||
@@ -1109,6 +1271,9 @@ async fn list_objects_once(
|
||||
let config = config.clone();
|
||||
let session = session.clone();
|
||||
drop(connections);
|
||||
if uses_presto_like_information_schema_tables(&config.db_type) {
|
||||
return external_driver_presto_like_objects(session, config.as_ref(), database, schema).await;
|
||||
}
|
||||
return session
|
||||
.invoke::<Vec<db::ObjectInfo>>(
|
||||
"listObjects",
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn is_schema_aware(database_type: DatabaseType) -> bool {
|
||||
| DatabaseType::H2
|
||||
| DatabaseType::Snowflake
|
||||
| DatabaseType::Trino
|
||||
| DatabaseType::PrestoSql
|
||||
| DatabaseType::Hive
|
||||
| DatabaseType::Db2
|
||||
| DatabaseType::Tdengine
|
||||
|
||||
@@ -164,6 +164,7 @@ async fn main() {
|
||||
"/jdbc/drivers/maven",
|
||||
get(routes::jdbc::list_jdbc_maven_bundles).post(routes::jdbc::install_jdbc_driver_from_maven),
|
||||
)
|
||||
.route("/jdbc/drivers/prestosql", post(routes::jdbc::install_prestosql_jdbc_driver))
|
||||
.route("/jdbc/drivers/maven/{bundle_id}", delete(routes::jdbc::delete_jdbc_maven_bundle))
|
||||
.route("/jdbc/drivers/{name}", delete(routes::jdbc::delete_jdbc_driver))
|
||||
.route("/jdbc/plugin/status", get(routes::jdbc::get_jdbc_plugin_status))
|
||||
|
||||
@@ -4,6 +4,7 @@ use axum::extract::{Multipart, Path, State};
|
||||
use axum::Json;
|
||||
use dbx_core::agent_service::AgentProgressEvent;
|
||||
use dbx_core::jdbc::{self, JdbcDriverInfo, JdbcMavenBundleInfo, JdbcMavenInstallRequest, JdbcPluginStatus};
|
||||
use dbx_core::plugins::PluginRuntimeEnv;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::error::AppError;
|
||||
@@ -28,7 +29,18 @@ pub async fn install_jdbc_driver_from_maven(
|
||||
Json(req): Json<JdbcMavenInstallRequest>,
|
||||
) -> Result<Json<Vec<JdbcDriverInfo>>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
Ok(Json(jdbc::install_jdbc_driver_from_maven(root, req).await.map_err(AppError::internal)?))
|
||||
Ok(Json(
|
||||
jdbc::install_jdbc_driver_from_maven(root, req, PluginRuntimeEnv::default())
|
||||
.await
|
||||
.map_err(AppError::internal)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn install_prestosql_jdbc_driver(
|
||||
State(state): State<Arc<WebState>>,
|
||||
) -> Result<Json<Vec<JdbcDriverInfo>>, AppError> {
|
||||
let root = state.app.plugins.root_dir();
|
||||
Ok(Json(jdbc::install_prestosql_jdbc_driver(root).await.map_err(AppError::internal)?))
|
||||
}
|
||||
|
||||
pub async fn import_jdbc_drivers(
|
||||
|
||||
@@ -46,7 +46,7 @@ function formatQueryToolResult(result: QueryResult, title?: string) {
|
||||
}
|
||||
|
||||
export const DBX_CONNECTION_TYPE_DESCRIPTION =
|
||||
"Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, tdengine, iotdb, xugu, jdbc, access, mq";
|
||||
"Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, milvus, qdrant, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, prestosql, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, tdengine, iotdb, xugu, jdbc, access, mq";
|
||||
const FILE_CAPABLE_CONNECTION_TYPES = new Set(["sqlite", "duckdb", "access", "h2"]);
|
||||
|
||||
interface McpScope {
|
||||
|
||||
@@ -43,6 +43,7 @@ export const BRIDGE_REQUIRED_TYPES = [
|
||||
"h2",
|
||||
"snowflake",
|
||||
"trino",
|
||||
"prestosql",
|
||||
"hive",
|
||||
"db2",
|
||||
"informix",
|
||||
|
||||
@@ -1093,6 +1093,9 @@ public final class DbxJdbcPlugin {
|
||||
if (quirks.useOracleMetadata()) {
|
||||
return oracleListTables(conn, oracleEffectiveSchema(conn, schema));
|
||||
}
|
||||
if (usePrestoInformationSchemaTables(connection)) {
|
||||
return prestoListTables(conn, database, schema);
|
||||
}
|
||||
DatabaseMetaData meta = conn.getMetaData();
|
||||
String[] types = jdbcTableTypes(meta);
|
||||
String catalog = metadataCatalog(database, quirks);
|
||||
@@ -1110,6 +1113,9 @@ public final class DbxJdbcPlugin {
|
||||
if (driverQuirks(connection).useOracleMetadata()) {
|
||||
return oracleListObjects(conn, oracleEffectiveSchema(conn, schema), schema);
|
||||
}
|
||||
if (usePrestoInformationSchemaTables(connection)) {
|
||||
return prestoListObjects(conn, database, schema);
|
||||
}
|
||||
DatabaseMetaData meta = conn.getMetaData();
|
||||
JdbcDriverQuirks quirks = driverQuirks(connection);
|
||||
String catalog = metadataCatalog(database, quirks);
|
||||
@@ -1324,6 +1330,72 @@ public final class DbxJdbcPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean usePrestoInformationSchemaTables(JsonNode connection) {
|
||||
String url = optionalText(connection, "connection_string");
|
||||
return urlMatchesPrefix(url, "jdbc:presto:") || urlMatchesPrefix(url, "jdbc:trino:");
|
||||
}
|
||||
|
||||
private static JsonNode prestoListTables(Connection conn, String database, String schema) throws SQLException {
|
||||
ArrayNode result = MAPPER.createArrayNode();
|
||||
try (PreparedStatement ps = conn.prepareStatement(prestoInformationSchemaTablesSql(database))) {
|
||||
ps.setString(1, schema);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
ObjectNode item = MAPPER.createObjectNode();
|
||||
item.put("name", rs.getString(1));
|
||||
item.put("table_type", normalizeInformationSchemaTableType(rs.getString(2)));
|
||||
item.putNull("comment");
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static JsonNode prestoListObjects(Connection conn, String database, String schema) throws SQLException {
|
||||
ArrayNode result = MAPPER.createArrayNode();
|
||||
try (PreparedStatement ps = conn.prepareStatement(prestoInformationSchemaTablesSql(database))) {
|
||||
ps.setString(1, schema);
|
||||
try (ResultSet rs = ps.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
ObjectNode item = MAPPER.createObjectNode();
|
||||
item.put("name", rs.getString(1));
|
||||
item.put("object_type", normalizeInformationSchemaTableType(rs.getString(2)));
|
||||
putNullable(item, "schema", schema);
|
||||
item.putNull("comment");
|
||||
result.add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static String prestoInformationSchemaTablesSql(String database) {
|
||||
String source = emptyToNull(database) == null
|
||||
? "information_schema.tables"
|
||||
: quoteAnsiIdentifier(database) + ".information_schema.tables";
|
||||
return "SELECT table_name, table_type FROM " + source +
|
||||
" WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')" +
|
||||
" ORDER BY table_type, table_name";
|
||||
}
|
||||
|
||||
static String normalizeInformationSchemaTableType(String tableType) {
|
||||
if (tableType == null) {
|
||||
return "TABLE";
|
||||
}
|
||||
String normalized = tableType.trim().toUpperCase(Locale.ROOT).replace(' ', '_');
|
||||
return switch (normalized) {
|
||||
case "BASE_TABLE" -> "TABLE";
|
||||
case "MATERIALIZED_VIEW" -> "MATERIALIZED_VIEW";
|
||||
case "VIEW" -> "VIEW";
|
||||
default -> tableType;
|
||||
};
|
||||
}
|
||||
|
||||
private static String quoteAnsiIdentifier(String identifier) {
|
||||
return "\"" + identifier.replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
private static void appendColumns(
|
||||
ArrayNode result,
|
||||
DatabaseMetaData meta,
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.sql.Date;
|
||||
import java.sql.Driver;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.DriverPropertyInfo;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
@@ -661,6 +662,41 @@ final class DbxJdbcPluginTest {
|
||||
assertEquals("姓名", columns.path(0).path("comment").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
void prestoListTablesUsesInformationSchemaInsteadOfJdbcMetadata() throws Exception {
|
||||
List<String> calls = new ArrayList<>();
|
||||
Driver driver = new PrestoMetadataDriver(calls);
|
||||
DriverManager.registerDriver(driver);
|
||||
try {
|
||||
JsonNode response = request("listTables", """
|
||||
{
|
||||
"connection": {
|
||||
"connection_string": "jdbc:presto://presto.example.test:8080/hive",
|
||||
"connect_timeout_secs": 30
|
||||
},
|
||||
"database": "hive",
|
||||
"schema": "sales_analytics"
|
||||
}
|
||||
""");
|
||||
|
||||
assertFalse(response.has("error"), response.toString());
|
||||
assertEquals("daily_revenue", response.path("result").path(0).path("name").asText());
|
||||
assertEquals("TABLE", response.path("result").path(0).path("table_type").asText());
|
||||
assertEquals("revenue_view", response.path("result").path(1).path("name").asText());
|
||||
assertEquals("VIEW", response.path("result").path(1).path("table_type").asText());
|
||||
assertEquals(
|
||||
List.of(
|
||||
"prepare:SELECT table_name, table_type FROM \"hive\".information_schema.tables WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW') ORDER BY table_type, table_name",
|
||||
"setString:1:sales_analytics",
|
||||
"executeQuery"
|
||||
),
|
||||
calls
|
||||
);
|
||||
} finally {
|
||||
DriverManager.deregisterDriver(driver);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void oracleMetadataObjectTypeAcceptsPackageBodyAliases() throws Exception {
|
||||
Method method = DbxJdbcPlugin.class.getDeclaredMethod("oracleMetadataObjectType", String.class);
|
||||
@@ -908,6 +944,112 @@ final class DbxJdbcPluginTest {
|
||||
);
|
||||
}
|
||||
|
||||
private static final class PrestoMetadataDriver implements Driver {
|
||||
private final List<String> calls;
|
||||
|
||||
private PrestoMetadataDriver(List<String> calls) {
|
||||
this.calls = calls;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection connect(String url, Properties info) throws SQLException {
|
||||
if (!acceptsURL(url)) {
|
||||
return null;
|
||||
}
|
||||
return prestoMetadataConnection(calls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean acceptsURL(String url) {
|
||||
return url != null && url.startsWith("jdbc:presto:");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
|
||||
return new DriverPropertyInfo[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMajorVersion() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinorVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean jdbcCompliant() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.logging.Logger getParentLogger() {
|
||||
return java.util.logging.Logger.getGlobal();
|
||||
}
|
||||
}
|
||||
|
||||
private static Connection prestoMetadataConnection(List<String> calls) {
|
||||
return (Connection) Proxy.newProxyInstance(
|
||||
DbxJdbcPluginTest.class.getClassLoader(),
|
||||
new Class<?>[] { Connection.class },
|
||||
(proxy, method, args) -> switch (method.getName()) {
|
||||
case "prepareStatement" -> {
|
||||
calls.add("prepare:" + args[0]);
|
||||
yield prestoMetadataStatement(calls);
|
||||
}
|
||||
case "getMetaData" -> throw new SQLException("DatabaseMetaData should not be used for Presto listTables");
|
||||
case "isClosed" -> false;
|
||||
case "close" -> null;
|
||||
default -> defaultValue(method.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static PreparedStatement prestoMetadataStatement(List<String> calls) {
|
||||
return (PreparedStatement) Proxy.newProxyInstance(
|
||||
DbxJdbcPluginTest.class.getClassLoader(),
|
||||
new Class<?>[] { PreparedStatement.class },
|
||||
(proxy, method, args) -> switch (method.getName()) {
|
||||
case "setString" -> {
|
||||
calls.add("setString:" + args[0] + ":" + args[1]);
|
||||
yield null;
|
||||
}
|
||||
case "executeQuery" -> {
|
||||
calls.add("executeQuery");
|
||||
yield prestoMetadataResultSet();
|
||||
}
|
||||
case "close" -> null;
|
||||
default -> defaultValue(method.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static ResultSet prestoMetadataResultSet() {
|
||||
String[] labels = { "table_name", "table_type" };
|
||||
String[][] rows = { { "daily_revenue", "BASE TABLE" }, { "revenue_view", "VIEW" } };
|
||||
return (ResultSet) Proxy.newProxyInstance(
|
||||
DbxJdbcPluginTest.class.getClassLoader(),
|
||||
new Class<?>[] { ResultSet.class },
|
||||
new java.lang.reflect.InvocationHandler() {
|
||||
private int index = -1;
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
return switch (method.getName()) {
|
||||
case "next" -> ++index < rows.length;
|
||||
case "getMetaData" -> resultSetMeta(labels);
|
||||
case "getString" -> rows[index][((Integer) args[0]) - 1];
|
||||
case "getObject" -> rows[index][((Integer) args[0]) - 1];
|
||||
case "close" -> null;
|
||||
default -> defaultValue(method.getReturnType());
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static final class BrokenResultSetDriver implements Driver {
|
||||
private final String urlPrefix;
|
||||
private final boolean executeReturnsResultSet;
|
||||
|
||||
@@ -9,8 +9,8 @@ pub use dbx_core::agent_connection::{
|
||||
};
|
||||
pub use dbx_core::connection::{
|
||||
agent_connect_timeout, connect_bare_metadata_pool, connect_mysql_metadata_pool, connection_url_for_endpoint,
|
||||
metadata_connection_config, probe_connection_endpoint, redacted_connection_url_for_endpoint, AppState, MysqlMode,
|
||||
PoolKind,
|
||||
metadata_connection_config, prestosql_jdbc_config_for_endpoint, probe_connection_endpoint,
|
||||
redacted_connection_url_for_endpoint, AppState, MysqlMode, PoolKind,
|
||||
};
|
||||
use dbx_core::database_capabilities;
|
||||
use dbx_core::db;
|
||||
@@ -730,6 +730,10 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
|
||||
db_type if database_capabilities::is_agent_type(&db_type) => {
|
||||
test_agent_connection(state.inner(), &config, &host, port).await
|
||||
}
|
||||
DatabaseType::PrestoSql => {
|
||||
let jdbc_config = prestosql_jdbc_config_for_endpoint(&config, &host, port);
|
||||
state.test_external_driver("jdbc", &jdbc_config).await
|
||||
}
|
||||
DatabaseType::Jdbc => {
|
||||
let mut jdbc_config = config.clone();
|
||||
if host != config.host || port != config.port {
|
||||
@@ -987,6 +991,10 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
|
||||
db_type if database_capabilities::is_agent_type(&db_type) => {
|
||||
connect_agent_pool(state.inner(), &db_config, &host, port).await?
|
||||
}
|
||||
DatabaseType::PrestoSql => {
|
||||
let jdbc_config = prestosql_jdbc_config_for_endpoint(&db_config, &host, port);
|
||||
state.external_driver_pool("jdbc", &jdbc_config).await?
|
||||
}
|
||||
DatabaseType::Jdbc => state.external_driver_pool("jdbc", &db_config).await?,
|
||||
db_type => return Err(format!("Unsupported database type: {db_type:?}")),
|
||||
};
|
||||
|
||||
@@ -72,7 +72,13 @@ pub async fn install_jdbc_driver_from_maven(
|
||||
state: State<'_, Arc<AppState>>,
|
||||
request: JdbcMavenInstallRequest,
|
||||
) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
jdbc::install_jdbc_driver_from_maven(state.plugins.root_dir(), request).await
|
||||
let env = state.external_driver_runtime_env("jdbc")?;
|
||||
jdbc::install_jdbc_driver_from_maven(state.plugins.root_dir(), request, env).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_prestosql_jdbc_driver(state: State<'_, Arc<AppState>>) -> Result<Vec<JdbcDriverInfo>, String> {
|
||||
jdbc::install_prestosql_jdbc_driver(state.plugins.root_dir()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
|
||||
@@ -433,6 +433,7 @@ pub fn run() {
|
||||
commands::plugins::list_jdbc_maven_bundles,
|
||||
commands::plugins::import_jdbc_drivers,
|
||||
commands::plugins::install_jdbc_driver_from_maven,
|
||||
commands::plugins::install_prestosql_jdbc_driver,
|
||||
commands::plugins::delete_jdbc_driver,
|
||||
commands::plugins::delete_jdbc_maven_bundle,
|
||||
commands::plugins::jdbc_plugin_status,
|
||||
|
||||
Reference in New Issue
Block a user