feat: add PrestoSQL connector

This commit is contained in:
luoianun
2026-06-23 01:27:08 +08:00
parent 7910a2f68e
commit 8cc7a35c1a
40 changed files with 1323 additions and 123 deletions
@@ -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");
});
});
+1
View File
@@ -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,
+4
View File
@@ -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,
};
}
+4
View File
@@ -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 });
}
+33 -4
View File
@@ -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];
}
+1
View File
@@ -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",
+1 -1
View File
@@ -158,7 +158,7 @@ const AGENT_CATALOG: &[AgentCatalogEntry] = &[
AgentCatalogEntry {
db_type: DatabaseType::Trino,
key: "trino",
label: "Trino (Presto)",
label: "Trino",
store_visible: true,
profiles: &[],
},
+35 -6
View File
@@ -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"));
+15
View File
@@ -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"]);
}
}
+9 -2
View File
@@ -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,
+97 -38
View File
@@ -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();
+207
View File
@@ -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);
}
}
+6
View File
@@ -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)
}
+1 -1
View File
@@ -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);
}
+166 -1
View File
@@ -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
+1
View File
@@ -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))
+13 -1
View File
@@ -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(
+1 -1
View File
@@ -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 {
+1
View File
@@ -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;
+10 -2
View File
@@ -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:?}")),
};
+7 -1
View File
@@ -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]
+1
View File
@@ -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,