feat(jdbc): add managed Apache Phoenix profile

This commit is contained in:
Guoyu Su authored and GitHub committed 2026-08-06 23:16:12 +08:00
1 parent c835deb3f9
commit 69f4450aee
38 files changed
+1651 -77

No files matched your search

@@ -0,0 +1,163 @@
import { describe, expect, it, vi } from "vitest";
import {
createJdbcProductConnectionFieldsByMode,
createJdbcProductProfileRegistry,
ensureJdbcProductRuntimeDrivers,
isJdbcProductDefaultDriverClass,
jdbcProductManagedRuntimePaths,
rememberJdbcProductConnectionFields,
validateJdbcProductManagedDriver,
type JdbcProductProfileDefinition,
} from "@/lib/database/jdbcProductProfile";
import type { ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
import type { ConnectionConfig, JdbcMavenBundleInfo } from "@/types/database";
const directCoordinate = "com.example:fixture-direct:1.0.0";
const proxyCoordinate = "com.example:fixture-proxy:1.0.0";
const profile: JdbcProductProfileDefinition = {
id: "fixture",
label: "Fixture JDBC",
icon: "fixture",
category: "analytics",
port: 0,
user: "",
match: { dbType: "jdbc", driverProfile: "fixture" },
managedDriverId: "fixture",
runtimeLabelKey: "connection.fixtureRuntime",
driverManagerHintPrefixKey: "connection.fixtureHintPrefix",
driverManagerHintSuffixKey: "connection.fixtureHintSuffix",
docsLabelKey: "connection.fixtureDocs",
docsUrl: "https://example.com/docs",
missingDriverError: "Fixture driver is not installed.",
missingPluginError: "JDBC plugin is not installed.",
modes: [
{
id: "direct",
labelKey: "connection.fixtureDirect",
hintKey: "connection.fixtureDirectHint",
defaultConnectionString: "jdbc:fixture:direct",
defaultDriverClass: "com.example.DirectDriver",
managedCoordinates: [directCoordinate],
},
{
id: "proxy",
labelKey: "connection.fixtureProxy",
hintKey: "connection.fixtureProxyHint",
defaultConnectionString: "jdbc:fixture:proxy",
defaultDriverClass: "com.example.ProxyDriver",
managedCoordinates: [proxyCoordinate],
},
],
detectMode: (config) => (config.connection_string?.includes(":proxy") ? "proxy" : "direct"),
isCompatibleRuntimePath: (path, mode) => path.endsWith(`fixture-${mode}.jar`),
};
function config(mode: "direct" | "proxy" = "direct"): ConnectionConfig {
return {
id: "fixture-1",
name: "Fixture",
db_type: "jdbc",
driver_profile: "fixture",
host: "",
port: 0,
username: "",
password: "",
connection_string: `jdbc:fixture:${mode}`,
jdbc_driver_class: mode === "direct" ? "com.example.DirectDriver" : "com.example.ProxyDriver",
jdbc_driver_paths: [],
};
}
function bundle(coordinate: string): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
return {
id: coordinate.replaceAll(":", "_"),
coordinate,
scope: "runtime",
repositories: ["https://repo.example.com/"],
installed_at: "2026-08-06T00:00:00Z",
path: `/drivers/${artifactId}`,
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path: `/drivers/${artifactId}-${version}.jar`,
size: 1,
sha256: coordinate,
},
],
};
}
describe("jdbcProductProfile", () => {
it("rejects duplicate profile ids and matchers", () => {
expect(() => createJdbcProductProfileRegistry([{ ...profile, id: "" }])).toThrow(/id must not be empty/);
expect(() => createJdbcProductProfileRegistry([profile, { ...profile }])).toThrow(/Duplicate JDBC product profile id/);
expect(() => createJdbcProductProfileRegistry([profile, { ...profile, id: "fixture-copy" }])).toThrow(/Duplicate JDBC product profile matcher/);
});
it("requires every profile runtime coordinate to be owned by its managed driver", () => {
const driver: ManagedJdbcDriverDefinition = {
id: "fixture",
label: "Fixture JDBC",
version: "1.0.0",
jre: "21",
bundles: [
{ coordinate: directCoordinate, repositories: [] },
{ coordinate: proxyCoordinate, repositories: [] },
],
};
expect(() => validateJdbcProductManagedDriver(profile, driver)).not.toThrow();
expect(() => validateJdbcProductManagedDriver(profile, undefined)).toThrow(/unknown managed driver/);
expect(() => validateJdbcProductManagedDriver(profile, { ...driver, bundles: driver.bundles.slice(0, 1) })).toThrow(/requires coordinate not owned/);
});
it("provides reusable per-mode defaults and field memory", () => {
let fields = createJdbcProductConnectionFieldsByMode(profile);
fields = rememberJdbcProductConnectionFields(profile, fields, "direct", {
connectionString: "jdbc:fixture:custom",
driverClass: "com.example.CustomDriver",
});
expect(fields.direct).toEqual({ connectionString: "jdbc:fixture:custom", driverClass: "com.example.CustomDriver" });
expect(fields.proxy).toEqual({ connectionString: "jdbc:fixture:proxy", driverClass: "com.example.ProxyDriver" });
expect(isJdbcProductDefaultDriverClass(profile, "com.example.ProxyDriver")).toBe(true);
expect(isJdbcProductDefaultDriverClass(profile, "com.example.CustomDriver")).toBe(false);
});
it("selects the registered mode runtime while preserving unrelated paths", async () => {
const direct = bundle(directCoordinate);
const proxy = bundle(proxyCoordinate);
const connection = config("proxy");
connection.jdbc_driver_paths = [direct.artifacts[0].path, "/etc/fixture"];
const api = {
jdbcPluginStatus: vi.fn(async () => ({ installed: true, compatible: true })),
listJdbcMavenBundles: vi.fn(async () => [direct, proxy]),
};
const result = await ensureJdbcProductRuntimeDrivers(profile, connection, api);
expect(jdbcProductManagedRuntimePaths(profile, [direct, proxy], "proxy")).toEqual([proxy.artifacts[0].path]);
expect(result?.paths).toEqual(["/etc/fixture", proxy.artifacts[0].path]);
expect(result?.runtimeSelectionId).toBe("fixture:proxy");
});
it("preserves an explicitly selected compatible custom runtime", async () => {
const connection = config();
connection.jdbc_driver_paths = ["/opt/fixture-direct.jar", "/etc/fixture"];
const result = await ensureJdbcProductRuntimeDrivers(profile, connection, {
jdbcPluginStatus: vi.fn(async () => ({ installed: true, compatible: true })),
listJdbcMavenBundles: vi.fn(async () => []),
});
expect(result?.paths).toEqual(connection.jdbc_driver_paths);
expect(result?.runtimeSelectionId).toBeUndefined();
});
});
@@ -0,0 +1,125 @@
import { describe, expect, it, vi } from "vitest";
import {
createManagedJdbcDriverRegistry,
installManagedJdbcDriver,
managedJdbcDriverBundles,
managedJdbcDriverPaths,
managedJdbcDriverRow,
missingManagedJdbcDriverBundles,
uninstallManagedJdbcDriver,
type ManagedJdbcDriverApi,
type ManagedJdbcDriverDefinition,
} from "@/lib/database/managedJdbcDriver";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
const definition: ManagedJdbcDriverDefinition = {
id: "fixture",
label: "Fixture JDBC",
version: "1.0.0",
jre: "21",
bundles: [
{ coordinate: "com.example:fixture-core:1.0.0", repositories: ["https://repo.example.com/"] },
{ coordinate: "com.example:fixture-extra:1.0.0", repositories: ["https://repo.example.com/"] },
],
};
function bundle(coordinate: string, size = 10): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
return {
id: coordinate.replaceAll(":", "_"),
coordinate,
scope: "runtime",
repositories: ["https://repo.example.com/"],
installed_at: "2026-08-06T00:00:00Z",
path: `/drivers/${artifactId}`,
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path: `/drivers/${artifactId}-${version}.jar`,
size,
sha256: coordinate,
},
],
};
}
function pluginStatus(installed = true): JdbcPluginStatus {
return {
installed,
version: installed ? "1.0.0" : null,
protocol_version: installed ? 1 : null,
compatible: installed,
latest_version: null,
latest_protocol_version: null,
update_available: false,
path: "/plugins/jdbc",
};
}
function api(overrides: Partial<ManagedJdbcDriverApi> = {}): ManagedJdbcDriverApi {
return {
installJdbcPlugin: vi.fn(async () => pluginStatus()),
installJdbcDriverFromMaven: vi.fn(async () => [] as JdbcDriverInfo[]),
listJdbcMavenBundles: vi.fn(async () => []),
deleteJdbcMavenBundle: vi.fn(async () => [] as JdbcDriverInfo[]),
...overrides,
};
}
describe("managedJdbcDriver", () => {
it("rejects duplicate ids and ambiguous Maven bundle ownership", () => {
expect(() => createManagedJdbcDriverRegistry([{ ...definition, id: "" }])).toThrow(/id must not be empty/);
expect(() => createManagedJdbcDriverRegistry([definition, { ...definition }])).toThrow(/Duplicate managed JDBC driver id/);
expect(() => createManagedJdbcDriverRegistry([definition, { ...definition, id: "fixture-copy" }])).toThrow(/is owned by both/);
});
it("derives exact bundle state, paths, size, and plugin-aware installation", () => {
const core = bundle(definition.bundles[0].coordinate, 12);
const extra = bundle(definition.bundles[1].coordinate, 18);
const unrelated = bundle("com.example:unrelated:1.0.0", 999);
expect(managedJdbcDriverBundles(definition, [unrelated, extra, core])).toEqual([core, extra]);
expect(missingManagedJdbcDriverBundles(definition, [core])).toEqual([definition.bundles[1]]);
expect(managedJdbcDriverPaths(definition, [core, extra])).toEqual([core.artifacts[0].path, extra.artifacts[0].path]);
expect(managedJdbcDriverPaths(definition, [core])).toEqual([]);
expect(managedJdbcDriverRow(definition, [core, extra], pluginStatus())).toMatchObject({ installed: true, size: 30, installed_version: "1.0.0" });
expect(managedJdbcDriverRow(definition, [core, extra])).toMatchObject({ installed: false, size: 30, installed_version: null });
expect(managedJdbcDriverRow(definition, [core, extra], pluginStatus(false))).toMatchObject({ installed: false, size: 30, installed_version: null });
});
it("ensures the plugin and installs only missing coordinates", async () => {
const core = bundle(definition.bundles[0].coordinate);
const extra = bundle(definition.bundles[1].coordinate);
const installJdbcPlugin = vi.fn(async () => pluginStatus());
const installJdbcDriverFromMaven = vi.fn(async () => [] as JdbcDriverInfo[]);
const listJdbcMavenBundles = vi.fn(async () => [core, extra]);
const runtimeApi = api({ installJdbcPlugin, installJdbcDriverFromMaven, listJdbcMavenBundles });
const result = await installManagedJdbcDriver(definition, [core], pluginStatus(false), runtimeApi);
expect(installJdbcPlugin).toHaveBeenCalledOnce();
expect(installJdbcDriverFromMaven).toHaveBeenCalledOnce();
expect(installJdbcDriverFromMaven).toHaveBeenCalledWith(definition.bundles[1].coordinate, ["https://repo.example.com/"]);
expect(result.bundles).toEqual([core, extra]);
expect(result.pluginStatus?.installed).toBe(true);
});
it("uninstalls only bundles owned by the selected definition", async () => {
const core = bundle(definition.bundles[0].coordinate);
const extra = bundle(definition.bundles[1].coordinate);
const unrelated = bundle("com.example:unrelated:1.0.0");
const deleteJdbcMavenBundle = vi.fn(async () => [] as JdbcDriverInfo[]);
const listJdbcMavenBundles = vi.fn(async () => [unrelated]);
const runtimeApi = api({ deleteJdbcMavenBundle, listJdbcMavenBundles });
const result = await uninstallManagedJdbcDriver(definition, [unrelated, core, extra], runtimeApi);
expect(deleteJdbcMavenBundle.mock.calls).toEqual([[core.id], [extra.id]]);
expect(result.bundles).toEqual([unrelated]);
});
});
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from "vitest";
import { installRegisteredManagedJdbcDriver, managedJdbcDriverDefinition, managedJdbcDriverRows } from "@/lib/database/managedJdbcDrivers";
import { PHOENIX_MANAGED_MAVEN_COORDINATES } from "@/lib/database/phoenixBuiltinDriver";
import { PRESTOSQL_JDBC_DRIVER_COORDINATE } from "@/lib/database/prestoSqlBuiltinDriver";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
function pluginStatus(): JdbcPluginStatus {
return {
installed: true,
version: "1.0.0",
protocol_version: 1,
compatible: true,
latest_version: null,
latest_protocol_version: null,
update_available: false,
path: "/plugins/jdbc",
};
}
describe("managedJdbcDrivers", () => {
it("registers Phoenix and PrestoSQL through the same row model", () => {
expect(managedJdbcDriverDefinition("phoenix")?.bundles.map(({ coordinate }) => coordinate)).toEqual(PHOENIX_MANAGED_MAVEN_COORDINATES);
expect(managedJdbcDriverDefinition("prestosql")?.bundles.map(({ coordinate }) => coordinate)).toEqual([PRESTOSQL_JDBC_DRIVER_COORDINATE]);
expect(managedJdbcDriverRows([], pluginStatus()).map(({ db_type }) => db_type)).toEqual(["prestosql", "phoenix"]);
});
it("installs PrestoSQL through the generic Maven resolver", async () => {
const installed: JdbcMavenBundleInfo = {
id: "io.prestosql_presto-jdbc_350",
coordinate: PRESTOSQL_JDBC_DRIVER_COORDINATE,
scope: "runtime",
repositories: ["https://repo.maven.apache.org/maven2/"],
installed_at: "2026-08-06T00:00:00Z",
path: "/drivers/presto",
artifacts: [],
};
const installJdbcDriverFromMaven = vi.fn(async () => [] as JdbcDriverInfo[]);
const result = await installRegisteredManagedJdbcDriver("prestosql", [], pluginStatus(), {
installJdbcPlugin: vi.fn(async () => pluginStatus()),
installJdbcDriverFromMaven,
listJdbcMavenBundles: vi.fn(async () => [installed]),
deleteJdbcMavenBundle: vi.fn(async () => [] as JdbcDriverInfo[]),
});
expect(installJdbcDriverFromMaven).toHaveBeenCalledWith(PRESTOSQL_JDBC_DRIVER_COORDINATE, ["https://repo.maven.apache.org/maven2/"]);
expect(result?.bundles).toEqual([installed]);
});
});
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { PHOENIX_JDBC_DRIVER_VERSION, PHOENIX_MANAGED_JDBC_DRIVER, PHOENIX_MANAGED_MAVEN_COORDINATES } from "@/lib/database/phoenixBuiltinDriver";
import { PHOENIX_MAVEN_REPOSITORY } from "@/lib/database/phoenixConnection";
import { managedJdbcDriverBundles, managedJdbcDriverRow, missingManagedJdbcDriverBundles } from "@/lib/database/managedJdbcDriver";
import type { JdbcMavenBundleInfo } from "@/types/database";
function bundle(coordinate: string, size = 100): JdbcMavenBundleInfo {
const [groupId, artifactId, version] = coordinate.split(":");
return {
id: `bundle:${coordinate}`,
coordinate,
scope: "runtime",
repositories: [PHOENIX_MAVEN_REPOSITORY],
installed_at: "2026-08-05T00:00:00Z",
path: "drivers/jdbc",
artifacts: [
{
group_id: groupId,
artifact_id: artifactId,
version,
classifier: "",
extension: "jar",
file_name: `${artifactId}-${version}.jar`,
path: `drivers/jdbc/${artifactId}-${version}.jar`,
size,
sha256: coordinate,
},
],
};
}
describe("phoenixBuiltinDriver", () => {
it("shows one uninstalled Apache Phoenix JDBC row when no managed bundle exists", () => {
const row = managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, []);
expect(row.db_type).toBe("phoenix");
expect(row.label).toBe("Apache Phoenix JDBC");
expect(row.version).toBe(PHOENIX_JDBC_DRIVER_VERSION);
expect(row.installed).toBe(false);
expect(row.installed_version).toBeNull();
expect(row.jre).toBe("21");
expect(missingManagedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, []).map(({ coordinate }) => coordinate)).toEqual(PHOENIX_MANAGED_MAVEN_COORDINATES);
});
it("stays uninstalled for a partial runtime and reports only missing coordinates", () => {
const direct = bundle(PHOENIX_MANAGED_MAVEN_COORDINATES[0], 120);
const row = managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, [direct]);
expect(row.installed).toBe(false);
expect(row.size).toBe(120);
expect(managedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [direct])).toEqual([direct]);
expect(missingManagedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [direct]).map(({ coordinate }) => coordinate)).toEqual(PHOENIX_MANAGED_MAVEN_COORDINATES.slice(1));
});
it("is installed only when Direct, logging, and PQS bundles are all present", () => {
const unrelated = bundle("com.mysql:mysql-connector-j:9.2.0", 999);
const managed = PHOENIX_MANAGED_MAVEN_COORDINATES.map((coordinate, index) => bundle(coordinate, index + 1));
const row = managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, [unrelated, ...managed], { installed: true, compatible: true });
expect(row.installed).toBe(true);
expect(row.installed_version).toBe(PHOENIX_JDBC_DRIVER_VERSION);
expect(row.size).toBe(6);
expect(managedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [unrelated, ...managed])).toEqual(managed);
expect(missingManagedJdbcDriverBundles(PHOENIX_MANAGED_JDBC_DRIVER, [unrelated, ...managed])).toEqual([]);
});
it("stays uninstalled when all bundles exist but the shared JDBC plugin is unavailable", () => {
const managed = PHOENIX_MANAGED_MAVEN_COORDINATES.map((coordinate) => bundle(coordinate));
expect(managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, managed, { installed: false, compatible: true }).installed).toBe(false);
expect(managedJdbcDriverRow(PHOENIX_MANAGED_JDBC_DRIVER, managed, { installed: true, compatible: false }).installed).toBe(false);
});
});
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { PRESTOSQL_JDBC_DRIVER_COORDINATE, prestoSqlBuiltinDriverPaths, prestoSqlBuiltinDriverRow, prestoSqlMavenBundle } from "@/lib/database/prestoSqlBuiltinDriver";
import { PRESTOSQL_JDBC_DRIVER_COORDINATE, PRESTOSQL_MANAGED_JDBC_DRIVER, prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver";
import { managedJdbcDriverBundles, managedJdbcDriverRow } from "@/lib/database/managedJdbcDriver";
import type { JdbcMavenBundleInfo } from "@/types/database";
function bundle(coordinate: string, version = "350"): JdbcMavenBundleInfo {
@@ -28,7 +29,7 @@ function bundle(coordinate: string, version = "350"): JdbcMavenBundleInfo {
describe("prestoSqlBuiltinDriver", () => {
it("adds an uninstalled PrestoSQL row when the Maven bundle is absent", () => {
const row = prestoSqlBuiltinDriverRow([]);
const row = managedJdbcDriverRow(PRESTOSQL_MANAGED_JDBC_DRIVER, []);
expect(row.db_type).toBe("prestosql");
expect(row.label).toBe("PrestoSQL");
@@ -40,9 +41,9 @@ describe("prestoSqlBuiltinDriver", () => {
it("marks PrestoSQL installed when its Maven coordinate is present", () => {
const installed = bundle(PRESTOSQL_JDBC_DRIVER_COORDINATE);
const row = prestoSqlBuiltinDriverRow([bundle("com.mysql:mysql-connector-j:9.2.0"), installed]);
const row = managedJdbcDriverRow(PRESTOSQL_MANAGED_JDBC_DRIVER, [bundle("com.mysql:mysql-connector-j:9.2.0"), installed], { installed: true, compatible: true });
expect(prestoSqlMavenBundle([installed])?.id).toBe(installed.id);
expect(managedJdbcDriverBundles(PRESTOSQL_MANAGED_JDBC_DRIVER, [installed])[0]?.id).toBe(installed.id);
expect(prestoSqlBuiltinDriverPaths([installed])).toEqual(["drivers/jdbc/presto-jdbc-350.jar"]);
expect(row.installed).toBe(true);
expect(row.installed_version).toBe("350");
@@ -70,7 +70,7 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => {
it("covers all known agent driver keys with no stale entries", () => {
// Golden set — mirrors the store-visible entries in agent_catalog.rs
// plus EXTRA_DRIVER_STORE_ENTRIES (duckdb, kafka, rocketmq,
// rabbitmq, sqlserver-legacy) and the PrestoSQL built-in driver.
// rabbitmq, sqlserver-legacy) and the built-in JDBC driver rows.
// When an Agent driver is added to the catalog its key MUST appear here;
// removing a key from the map without removing it from the list below
// will fail this test.
@@ -89,6 +89,7 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => {
"exasol",
"hive",
"kylin",
"phoenix",
"prestosql",
"saphana",
"snowflake",
@@ -145,8 +146,9 @@ describe("AGENT_DRIVER_CATEGORY_MAP integrity", () => {
});
});
describe("prestosql matches ConnectionDialog category", () => {
it("is mapped to analytics (same as ConnectionDialog dbCategoryDefinitions)", () => {
describe("built-in JDBC drivers match ConnectionDialog categories", () => {
it("maps PrestoSQL and Apache Phoenix to analytics", () => {
expect(getCategoryForAgentDriver("prestosql")).toBe("analytics");
expect(getCategoryForAgentDriver("phoenix")).toBe("analytics");
});
});
@@ -47,6 +47,7 @@ export const AGENT_DRIVER_CATEGORY_MAP: Readonly<Record<string, DriverCategoryKe
"oceanbase-oracle": "domestic",
oracle: "sql",
oscar: "domestic",
phoenix: "analytics",
prestosql: "analytics",
rabbitmq: "mq",
rocketmq: "mq",
@@ -0,0 +1,232 @@
import type { ConnectionConfig, DatabaseType, JdbcMavenBundleInfo } from "@/types/database";
import type { ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
export type JdbcProductConnectionFields = {
connectionString: string;
driverClass: string;
};
export type JdbcProductConnectionFieldsByMode = Record<string, JdbcProductConnectionFields>;
export type JdbcProductModeDefinition = {
id: string;
labelKey: string;
hintKey: string;
defaultConnectionString: string;
defaultDriverClass: string;
managedCoordinates: readonly string[];
};
export type JdbcProductProfileDefinition = {
id: string;
label: string;
icon: string;
category: string;
port: number;
user: string;
match: {
dbType: DatabaseType;
driverProfile?: string;
};
managedDriverId: string;
runtimeLabelKey: string;
driverManagerHintPrefixKey: string;
driverManagerHintSuffixKey: string;
docsLabelKey: string;
docsUrl: string;
missingDriverError: string;
missingPluginError: string;
modes: readonly JdbcProductModeDefinition[];
detectMode: (config: Partial<Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">>) => string;
isCompatibleRuntimePath: (path: string, mode: string) => boolean;
};
export type JdbcProductRuntimeApi = {
jdbcPluginStatus: () => Promise<{ installed: boolean; compatible: boolean }>;
listJdbcMavenBundles: () => Promise<JdbcMavenBundleInfo[]>;
};
export type JdbcProductRuntimeResult = {
profile: JdbcProductProfileDefinition;
mode: JdbcProductModeDefinition;
bundles: JdbcMavenBundleInfo[];
paths: string[];
runtimeSelectionId?: string;
};
export type JdbcProductRuntimeErrorKind = "plugin-missing" | "driver-missing";
export class JdbcProductRuntimeError extends Error {
readonly kind: JdbcProductRuntimeErrorKind;
readonly profileId: string;
readonly managedDriverId: string;
constructor(kind: JdbcProductRuntimeErrorKind, profile: JdbcProductProfileDefinition) {
super(kind === "plugin-missing" ? profile.missingPluginError : profile.missingDriverError);
this.name = "JdbcProductRuntimeError";
this.kind = kind;
this.profileId = profile.id;
this.managedDriverId = profile.managedDriverId;
}
}
export type JdbcProductProfileRegistry = {
profiles: readonly JdbcProductProfileDefinition[];
byId: ReadonlyMap<string, JdbcProductProfileDefinition>;
};
export function createJdbcProductProfileRegistry(profiles: readonly JdbcProductProfileDefinition[]): JdbcProductProfileRegistry {
const byId = new Map<string, JdbcProductProfileDefinition>();
const matcherKeys = new Set<string>();
for (const profile of profiles) {
if (!profile.id.trim()) throw new Error("JDBC product profile id must not be empty");
if (byId.has(profile.id)) throw new Error(`Duplicate JDBC product profile id: ${profile.id}`);
const matcherKey = `${profile.match.dbType}:${profile.match.driverProfile ?? "*"}`;
if (matcherKeys.has(matcherKey)) throw new Error(`Duplicate JDBC product profile matcher: ${matcherKey}`);
if (profile.modes.length === 0) throw new Error(`JDBC product profile must define at least one mode: ${profile.id}`);
byId.set(profile.id, profile);
matcherKeys.add(matcherKey);
}
return { profiles: [...profiles], byId };
}
export function jdbcProductProfileMatches(profile: JdbcProductProfileDefinition, config: Pick<ConnectionConfig, "db_type" | "driver_profile">): boolean {
return config.db_type === profile.match.dbType && (profile.match.driverProfile === undefined || config.driver_profile === profile.match.driverProfile);
}
export function jdbcProductMode(profile: JdbcProductProfileDefinition, modeId: string): JdbcProductModeDefinition {
const mode = profile.modes.find((candidate) => candidate.id === modeId) ?? profile.modes[0];
if (!mode) throw new Error(`JDBC product profile has no modes: ${profile.id}`);
return mode;
}
export function jdbcProductConnectionDefaults(profile: JdbcProductProfileDefinition, modeId: string): JdbcProductConnectionFields {
const mode = jdbcProductMode(profile, modeId);
return {
connectionString: mode.defaultConnectionString,
driverClass: mode.defaultDriverClass,
};
}
export function createJdbcProductConnectionFieldsByMode(profile: JdbcProductProfileDefinition, config?: Partial<Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">>): JdbcProductConnectionFieldsByMode {
const fields = Object.fromEntries(profile.modes.map((mode) => [mode.id, jdbcProductConnectionDefaults(profile, mode.id)]));
if (!config) return fields;
const modeId = profile.detectMode(config);
const defaults = jdbcProductConnectionDefaults(profile, modeId);
fields[modeId] = {
connectionString: config.connection_string?.trim() || defaults.connectionString,
driverClass: config.jdbc_driver_class?.trim() || defaults.driverClass,
};
return fields;
}
export function rememberJdbcProductConnectionFields(profile: JdbcProductProfileDefinition, fields: JdbcProductConnectionFieldsByMode, modeId: string, current: Partial<JdbcProductConnectionFields>): JdbcProductConnectionFieldsByMode {
const defaults = jdbcProductConnectionDefaults(profile, modeId);
return {
...fields,
[modeId]: {
connectionString: current.connectionString?.trim() || defaults.connectionString,
driverClass: current.driverClass?.trim() || defaults.driverClass,
},
};
}
export function isJdbcProductDefaultDriverClass(profile: JdbcProductProfileDefinition, value: string | undefined): boolean {
const normalized = value?.trim() || "";
return !normalized || profile.modes.some((mode) => mode.defaultDriverClass === normalized);
}
export function jdbcProductManagedCoordinates(profile: JdbcProductProfileDefinition): string[] {
return Array.from(new Set(profile.modes.flatMap((mode) => [...mode.managedCoordinates])));
}
export function validateJdbcProductManagedDriver(profile: JdbcProductProfileDefinition, driver: ManagedJdbcDriverDefinition | undefined): void {
if (!driver || driver.id !== profile.managedDriverId) {
throw new Error(`JDBC product profile ${profile.id} references unknown managed driver: ${profile.managedDriverId}`);
}
const ownedCoordinates = new Set(driver.bundles.map(({ coordinate }) => coordinate));
const unownedCoordinate = jdbcProductManagedCoordinates(profile).find((coordinate) => !ownedCoordinates.has(coordinate));
if (unownedCoordinate) {
throw new Error(`JDBC product profile ${profile.id} requires coordinate not owned by ${driver.id}: ${unownedCoordinate}`);
}
}
export function isJdbcProductManagedMavenCoordinate(profile: JdbcProductProfileDefinition, coordinate: string): boolean {
return jdbcProductManagedCoordinates(profile).includes(coordinate);
}
export function jdbcProductManagedRuntimePaths(profile: JdbcProductProfileDefinition, bundles: readonly JdbcMavenBundleInfo[], modeId: string): string[] {
const bundleByCoordinate = new Map(bundles.map((bundle) => [bundle.coordinate, bundle]));
const requiredBundles = jdbcProductMode(profile, modeId).managedCoordinates.map((coordinate) => bundleByCoordinate.get(coordinate));
if (requiredBundles.some((bundle) => !bundle)) return [];
return requiredBundles.flatMap((bundle) => (bundle?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean));
}
export function jdbcProductRuntimeSelectionId(profile: JdbcProductProfileDefinition, modeId: string): string {
return `${profile.id}:${modeId}`;
}
function mavenBundleId(coordinate: string): string {
return coordinate
.trim()
.split("")
.map((character) => (/^[A-Za-z0-9.-]$/.test(character) ? character : "_"))
.join("");
}
export function isJdbcProductManagedMavenPath(profile: JdbcProductProfileDefinition, path: string): boolean {
const normalized = path.replaceAll("\\", "/");
return jdbcProductManagedCoordinates(profile).some((coordinate) => normalized.split("/").includes(mavenBundleId(coordinate)));
}
export function isJdbcProductRuntimeInstallError(profile: JdbcProductProfileDefinition, message: string): boolean {
return message.includes(profile.missingDriverError) || message.includes(profile.missingPluginError);
}
export async function ensureJdbcProductRuntimeDrivers(profile: JdbcProductProfileDefinition, config: ConnectionConfig, api: JdbcProductRuntimeApi): Promise<JdbcProductRuntimeResult | undefined> {
if (!jdbcProductProfileMatches(profile, config)) return undefined;
const modeId = profile.detectMode(config);
const mode = jdbcProductMode(profile, modeId);
const defaults = jdbcProductConnectionDefaults(profile, modeId);
config.connection_string = config.connection_string?.trim() || defaults.connectionString;
config.jdbc_driver_class = config.jdbc_driver_class?.trim() || defaults.driverClass;
const configuredPaths = (config.jdbc_driver_paths ?? []).map((path) => path.trim()).filter(Boolean);
const pluginStatus = await api.jdbcPluginStatus();
if (!pluginStatus.installed || !pluginStatus.compatible) {
throw new JdbcProductRuntimeError("plugin-missing", profile);
}
const bundles = await api.listJdbcMavenBundles();
const managedCoordinates = jdbcProductManagedCoordinates(profile);
const managedBundles = bundles.filter((bundle) => managedCoordinates.includes(bundle.coordinate));
const managedPaths = new Set(managedBundles.flatMap((bundle) => bundle.artifacts.map((artifact) => artifact.path).filter(Boolean)));
const hasCustomRuntime = configuredPaths.some((path) => profile.isCompatibleRuntimePath(path, modeId) && !managedPaths.has(path) && !isJdbcProductManagedMavenPath(profile, path));
if (hasCustomRuntime) {
config.jdbc_driver_paths = configuredPaths;
return { profile, mode, bundles, paths: configuredPaths };
}
const installedCoordinates = new Set(bundles.map((bundle) => bundle.coordinate));
if (mode.managedCoordinates.some((coordinate) => !installedCoordinates.has(coordinate))) {
throw new JdbcProductRuntimeError("driver-missing", profile);
}
const customPaths = configuredPaths.filter((path) => !managedPaths.has(path) && !isJdbcProductManagedMavenPath(profile, path));
const runtimePaths = jdbcProductManagedRuntimePaths(profile, bundles, modeId);
if (runtimePaths.length === 0) {
throw new JdbcProductRuntimeError("driver-missing", profile);
}
const paths = Array.from(new Set([...customPaths, ...runtimePaths]));
config.jdbc_driver_paths = paths;
return {
profile,
mode,
bundles,
paths,
runtimeSelectionId: jdbcProductRuntimeSelectionId(profile, modeId),
};
}
@@ -0,0 +1,57 @@
import type { ConnectionConfig } from "@/types/database";
import { PHOENIX_JDBC_PRODUCT_PROFILE } from "@/lib/database/phoenixConnection";
import { createJdbcProductProfileRegistry, ensureJdbcProductRuntimeDrivers, isJdbcProductRuntimeInstallError, jdbcProductProfileMatches, validateJdbcProductManagedDriver, type JdbcProductProfileDefinition, type JdbcProductRuntimeApi } from "@/lib/database/jdbcProductProfile";
import { managedJdbcDriverDefinition } from "@/lib/database/managedJdbcDrivers";
export const JDBC_PRODUCT_PROFILES = [PHOENIX_JDBC_PRODUCT_PROFILE] as const satisfies readonly JdbcProductProfileDefinition[];
const jdbcProductProfileRegistry = createJdbcProductProfileRegistry(JDBC_PRODUCT_PROFILES);
for (const profile of JDBC_PRODUCT_PROFILES) {
validateJdbcProductManagedDriver(profile, managedJdbcDriverDefinition(profile.managedDriverId));
}
export function jdbcProductProfileDefinition(id: string): JdbcProductProfileDefinition | undefined {
return jdbcProductProfileRegistry.byId.get(id);
}
export function jdbcProductProfileForConfig(config: Pick<ConnectionConfig, "db_type" | "driver_profile">): JdbcProductProfileDefinition | undefined {
return JDBC_PRODUCT_PROFILES.find((profile) => jdbcProductProfileMatches(profile, config));
}
export function jdbcProductDriverProfiles() {
return Object.fromEntries(
JDBC_PRODUCT_PROFILES.map((profile) => [
profile.id,
{
type: profile.match.dbType,
port: profile.port,
user: profile.user,
label: profile.label,
icon: profile.icon,
},
]),
);
}
export function jdbcProductPickerOptions() {
return JDBC_PRODUCT_PROFILES.map((profile) => ({ value: profile.id, label: profile.label }));
}
export function jdbcProductIconTypes(): Record<string, string> {
return Object.fromEntries(JDBC_PRODUCT_PROFILES.map((profile) => [profile.id, profile.icon]));
}
export function jdbcProductProfileIdsForCategory(category: string): string[] {
return JDBC_PRODUCT_PROFILES.filter((profile) => profile.category === category).map((profile) => profile.id);
}
export function isRegisteredJdbcProductRuntimeInstallError(config: Pick<ConnectionConfig, "db_type" | "driver_profile">, message: string): boolean {
const profile = jdbcProductProfileForConfig(config);
return Boolean(profile && isJdbcProductRuntimeInstallError(profile, message));
}
export async function ensureRegisteredJdbcProductRuntimeDrivers(config: ConnectionConfig, api: JdbcProductRuntimeApi) {
const profile = jdbcProductProfileForConfig(config);
if (!profile) return undefined;
return ensureJdbcProductRuntimeDrivers(profile, config, api);
}
@@ -0,0 +1,110 @@
import type { AgentDriverInfo } from "@/lib/backend/api";
import type { JdbcDriverInfo, JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
export type ManagedJdbcBundleDefinition = {
coordinate: string;
repositories: readonly string[];
};
export type ManagedJdbcDriverDefinition = {
id: string;
label: string;
version: string;
jre: string;
bundles: readonly ManagedJdbcBundleDefinition[];
};
export type ManagedJdbcDriverApi = {
installJdbcPlugin: () => Promise<JdbcPluginStatus>;
installJdbcDriverFromMaven: (coordinate: string, repositories?: string[]) => Promise<JdbcDriverInfo[]>;
listJdbcMavenBundles: () => Promise<JdbcMavenBundleInfo[]>;
deleteJdbcMavenBundle: (bundleId: string) => Promise<JdbcDriverInfo[]>;
};
export type ManagedJdbcDriverMutationResult = {
drivers?: JdbcDriverInfo[];
bundles: JdbcMavenBundleInfo[];
pluginStatus?: JdbcPluginStatus;
};
export type ManagedJdbcDriverRegistry = {
definitions: readonly ManagedJdbcDriverDefinition[];
byId: ReadonlyMap<string, ManagedJdbcDriverDefinition>;
};
export function createManagedJdbcDriverRegistry(definitions: readonly ManagedJdbcDriverDefinition[]): ManagedJdbcDriverRegistry {
const byId = new Map<string, ManagedJdbcDriverDefinition>();
const coordinateOwners = new Map<string, string>();
for (const definition of definitions) {
if (!definition.id.trim()) throw new Error("Managed JDBC driver id must not be empty");
if (byId.has(definition.id)) throw new Error(`Duplicate managed JDBC driver id: ${definition.id}`);
if (definition.bundles.length === 0) throw new Error(`Managed JDBC driver must own at least one bundle: ${definition.id}`);
for (const bundle of definition.bundles) {
const owner = coordinateOwners.get(bundle.coordinate);
if (owner) throw new Error(`Managed JDBC coordinate ${bundle.coordinate} is owned by both ${owner} and ${definition.id}`);
coordinateOwners.set(bundle.coordinate, definition.id);
}
byId.set(definition.id, definition);
}
return { definitions: [...definitions], byId };
}
export function managedJdbcDriverBundles(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[]): JdbcMavenBundleInfo[] {
const bundleByCoordinate = new Map(bundles.map((bundle) => [bundle.coordinate, bundle]));
return definition.bundles.map(({ coordinate }) => bundleByCoordinate.get(coordinate)).filter((bundle): bundle is JdbcMavenBundleInfo => Boolean(bundle));
}
export function missingManagedJdbcDriverBundles(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[]): ManagedJdbcBundleDefinition[] {
const installedCoordinates = new Set(bundles.map((bundle) => bundle.coordinate));
return definition.bundles.filter(({ coordinate }) => !installedCoordinates.has(coordinate));
}
export function managedJdbcDriverPaths(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[], coordinates: readonly string[] = definition.bundles.map(({ coordinate }) => coordinate)): string[] {
const bundleByCoordinate = new Map(managedJdbcDriverBundles(definition, bundles).map((bundle) => [bundle.coordinate, bundle]));
const requiredBundles = coordinates.map((coordinate) => bundleByCoordinate.get(coordinate));
if (requiredBundles.some((bundle) => !bundle)) return [];
return requiredBundles.flatMap((bundle) => (bundle?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean));
}
export function managedJdbcDriverRow(definition: ManagedJdbcDriverDefinition, bundles: readonly JdbcMavenBundleInfo[], pluginStatus?: Pick<JdbcPluginStatus, "installed" | "compatible"> | null): AgentDriverInfo {
const installedBundles = managedJdbcDriverBundles(definition, bundles);
const pluginReady = pluginStatus?.installed === true && pluginStatus.compatible;
const installed = pluginReady && installedBundles.length === definition.bundles.length;
return {
db_type: definition.id,
label: definition.label,
version: definition.version,
size: installedBundles.reduce((bundleTotal, bundle) => bundleTotal + bundle.artifacts.reduce((artifactTotal, artifact) => artifactTotal + Number(artifact.size || 0), 0), 0),
installed,
installed_version: installed ? definition.version : null,
update_available: false,
requires_java_runtime: true,
jre: definition.jre,
jre_installed: true,
};
}
export async function installManagedJdbcDriver(definition: ManagedJdbcDriverDefinition, currentBundles: readonly JdbcMavenBundleInfo[], currentPluginStatus: Pick<JdbcPluginStatus, "installed" | "compatible"> | null | undefined, api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult> {
let pluginStatus: JdbcPluginStatus | undefined;
if (!currentPluginStatus?.installed || !currentPluginStatus.compatible) {
pluginStatus = await api.installJdbcPlugin();
}
let bundles = [...currentBundles];
let drivers: JdbcDriverInfo[] | undefined;
for (const bundle of definition.bundles) {
if (bundles.some(({ coordinate }) => coordinate === bundle.coordinate)) continue;
drivers = await api.installJdbcDriverFromMaven(bundle.coordinate, [...bundle.repositories]);
bundles = await api.listJdbcMavenBundles();
}
return { drivers, bundles, pluginStatus };
}
export async function uninstallManagedJdbcDriver(definition: ManagedJdbcDriverDefinition, currentBundles: readonly JdbcMavenBundleInfo[], api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult> {
let drivers: JdbcDriverInfo[] | undefined;
for (const bundle of managedJdbcDriverBundles(definition, currentBundles)) {
drivers = await api.deleteJdbcMavenBundle(bundle.id);
}
const bundles = await api.listJdbcMavenBundles();
return { drivers, bundles };
}
@@ -0,0 +1,32 @@
import type { JdbcMavenBundleInfo, JdbcPluginStatus } from "@/types/database";
import { PHOENIX_MANAGED_JDBC_DRIVER } from "@/lib/database/phoenixBuiltinDriver";
import { PRESTOSQL_MANAGED_JDBC_DRIVER } from "@/lib/database/prestoSqlBuiltinDriver";
import { createManagedJdbcDriverRegistry, installManagedJdbcDriver, managedJdbcDriverRow, uninstallManagedJdbcDriver, type ManagedJdbcDriverApi, type ManagedJdbcDriverDefinition, type ManagedJdbcDriverMutationResult } from "@/lib/database/managedJdbcDriver";
export const MANAGED_JDBC_DRIVERS = [PRESTOSQL_MANAGED_JDBC_DRIVER, PHOENIX_MANAGED_JDBC_DRIVER] as const satisfies readonly ManagedJdbcDriverDefinition[];
const managedJdbcDriverRegistry = createManagedJdbcDriverRegistry(MANAGED_JDBC_DRIVERS);
export function managedJdbcDriverDefinition(id: string): ManagedJdbcDriverDefinition | undefined {
return managedJdbcDriverRegistry.byId.get(id);
}
export function isManagedJdbcDriver(id: string): boolean {
return managedJdbcDriverRegistry.byId.has(id);
}
export function managedJdbcDriverRows(bundles: readonly JdbcMavenBundleInfo[], pluginStatus?: Pick<JdbcPluginStatus, "installed" | "compatible"> | null) {
return MANAGED_JDBC_DRIVERS.map((definition) => managedJdbcDriverRow(definition, bundles, pluginStatus));
}
export async function installRegisteredManagedJdbcDriver(id: string, bundles: readonly JdbcMavenBundleInfo[], pluginStatus: Pick<JdbcPluginStatus, "installed" | "compatible"> | null | undefined, api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult | undefined> {
const definition = managedJdbcDriverDefinition(id);
if (!definition) return undefined;
return installManagedJdbcDriver(definition, bundles, pluginStatus, api);
}
export async function uninstallRegisteredManagedJdbcDriver(id: string, bundles: readonly JdbcMavenBundleInfo[], api: ManagedJdbcDriverApi): Promise<ManagedJdbcDriverMutationResult | undefined> {
const definition = managedJdbcDriverDefinition(id);
if (!definition) return undefined;
return uninstallManagedJdbcDriver(definition, bundles, api);
}
@@ -0,0 +1,16 @@
import { PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE, PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DRIVER_PROFILE, PHOENIX_QUERY_SERVER_MAVEN_COORDINATE } from "@/lib/database/phoenixConnection";
import type { ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
export const PHOENIX_JDBC_DRIVER_VERSION = "5.2.1 / 6.0.0";
export const PHOENIX_MANAGED_MAVEN_COORDINATES = [PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE, PHOENIX_QUERY_SERVER_MAVEN_COORDINATE] as const;
export const PHOENIX_MANAGED_JDBC_DRIVER: ManagedJdbcDriverDefinition = {
id: PHOENIX_DRIVER_PROFILE,
label: "Apache Phoenix JDBC",
version: PHOENIX_JDBC_DRIVER_VERSION,
jre: "21",
bundles: PHOENIX_MANAGED_MAVEN_COORDINATES.map((coordinate) => ({
coordinate,
repositories: ["https://repo.maven.apache.org/maven2/"],
})),
};
@@ -0,0 +1,68 @@
import type { ConnectionConfig } from "@/types/database";
import type { JdbcProductProfileDefinition } from "@/lib/database/jdbcProductProfile";
export const PHOENIX_DRIVER_PROFILE = "phoenix";
export const PHOENIX_DIRECT_JDBC_URL = "jdbc:phoenix:localhost";
export const PHOENIX_DIRECT_JDBC_DRIVER_CLASS = "org.apache.phoenix.jdbc.PhoenixDriver";
export const PHOENIX_QUERY_SERVER_JDBC_URL = "jdbc:phoenix:thin:url=http://127.0.0.1:8765;serialization=PROTOBUF";
export const PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS = "org.apache.phoenix.queryserver.client.Driver";
export const PHOENIX_MAVEN_REPOSITORY = "https://repo.maven.apache.org/maven2/";
export const PHOENIX_DIRECT_MAVEN_COORDINATE = "org.apache.phoenix:phoenix-client-embedded-hbase-2.5:5.2.1";
export const PHOENIX_QUERY_SERVER_MAVEN_COORDINATE = "org.apache.phoenix:phoenix-queryserver-client:6.0.0";
export const PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE = "ch.qos.reload4j:reload4j:1.2.26";
export const PHOENIX_DRIVER_NOT_INSTALLED_ERROR = "Apache Phoenix JDBC driver is not installed. Install it from the Driver Manager, then retry.";
export const PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR = "DBX JDBC plugin is not installed. Install Apache Phoenix JDBC from the Driver Manager, then retry.";
export type PhoenixConnectionMode = "direct" | "query-server";
type PhoenixConnectionConfig = Partial<Pick<ConnectionConfig, "connection_string" | "jdbc_driver_class">>;
export const PHOENIX_JDBC_PRODUCT_PROFILE: JdbcProductProfileDefinition = {
id: PHOENIX_DRIVER_PROFILE,
label: "Apache Phoenix",
icon: "phoenix",
category: "analytics",
port: 0,
user: "",
match: { dbType: "jdbc", driverProfile: PHOENIX_DRIVER_PROFILE },
managedDriverId: PHOENIX_DRIVER_PROFILE,
runtimeLabelKey: "connection.phoenixRuntimeOption",
driverManagerHintPrefixKey: "connection.phoenixDriverManagerHintPrefix",
driverManagerHintSuffixKey: "connection.phoenixDriverManagerHintSuffix",
docsLabelKey: "connection.phoenixDocs",
docsUrl: "https://phoenix.apache.org/docs/fundamentals/client-classpath-and-jdbc-url/",
missingDriverError: PHOENIX_DRIVER_NOT_INSTALLED_ERROR,
missingPluginError: PHOENIX_JDBC_PLUGIN_NOT_INSTALLED_ERROR,
modes: [
{
id: "direct",
labelKey: "connection.phoenixDirectMode",
hintKey: "connection.phoenixDirectModeHint",
defaultConnectionString: PHOENIX_DIRECT_JDBC_URL,
defaultDriverClass: PHOENIX_DIRECT_JDBC_DRIVER_CLASS,
managedCoordinates: [PHOENIX_DIRECT_MAVEN_COORDINATE, PHOENIX_DIRECT_LOGGING_MAVEN_COORDINATE],
},
{
id: "query-server",
labelKey: "connection.phoenixQueryServerMode",
hintKey: "connection.phoenixQueryServerModeHint",
defaultConnectionString: PHOENIX_QUERY_SERVER_JDBC_URL,
defaultDriverClass: PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS,
managedCoordinates: [PHOENIX_QUERY_SERVER_MAVEN_COORDINATE],
},
],
detectMode: (config) => phoenixConnectionModeForConfig(config),
isCompatibleRuntimePath: (path, mode) => isPhoenixRuntimePath(path, mode as PhoenixConnectionMode),
};
export function phoenixConnectionModeForConfig(config: PhoenixConnectionConfig): PhoenixConnectionMode {
const connectionString = config.connection_string?.trim().toLowerCase() || "";
const driverClass = config.jdbc_driver_class?.trim().toLowerCase() || "";
return connectionString.startsWith("jdbc:phoenix:thin:") || driverClass === PHOENIX_QUERY_SERVER_JDBC_DRIVER_CLASS.toLowerCase() ? "query-server" : "direct";
}
export function isPhoenixRuntimePath(path: string, mode: PhoenixConnectionMode): boolean {
const parts = path.split(/[/\\]/);
const fileName = parts[parts.length - 1] || path;
return mode === "direct" ? /^phoenix-client-(?:embedded-)?hbase-.+\.jar$/i.test(fileName) : /^phoenix-queryserver-client-.+\.jar$/i.test(fileName);
}
@@ -1,31 +1,23 @@
import type { AgentDriverInfo } from "@/lib/backend/api";
import type { JdbcMavenBundleInfo } from "@/types/database";
import { managedJdbcDriverPaths, type ManagedJdbcDriverDefinition } from "@/lib/database/managedJdbcDriver";
export const PRESTOSQL_DRIVER_DB_TYPE = "prestosql";
export const PRESTOSQL_JDBC_DRIVER_VERSION = "350";
export const PRESTOSQL_JDBC_DRIVER_COORDINATE = `io.prestosql:presto-jdbc:${PRESTOSQL_JDBC_DRIVER_VERSION}`;
export const PRESTOSQL_JDBC_DRIVER_REPOSITORY = "https://repo.maven.apache.org/maven2/";
export function prestoSqlMavenBundle(bundles: JdbcMavenBundleInfo[]): JdbcMavenBundleInfo | undefined {
return bundles.find((bundle) => bundle.coordinate === PRESTOSQL_JDBC_DRIVER_COORDINATE);
}
export const PRESTOSQL_MANAGED_JDBC_DRIVER: ManagedJdbcDriverDefinition = {
id: PRESTOSQL_DRIVER_DB_TYPE,
label: "PrestoSQL",
version: PRESTOSQL_JDBC_DRIVER_VERSION,
jre: "21",
bundles: [
{
coordinate: PRESTOSQL_JDBC_DRIVER_COORDINATE,
repositories: [PRESTOSQL_JDBC_DRIVER_REPOSITORY],
},
],
};
export function prestoSqlBuiltinDriverPaths(bundles: JdbcMavenBundleInfo[]): string[] {
return (prestoSqlMavenBundle(bundles)?.artifacts ?? []).map((artifact) => artifact.path).filter(Boolean);
}
export function prestoSqlBuiltinDriverRow(bundles: JdbcMavenBundleInfo[]): AgentDriverInfo {
const installedBundle = prestoSqlMavenBundle(bundles);
return {
db_type: PRESTOSQL_DRIVER_DB_TYPE,
label: "PrestoSQL",
version: PRESTOSQL_JDBC_DRIVER_VERSION,
size: installedBundle ? installedBundle.artifacts.reduce((total, artifact) => total + Number(artifact.size || 0), 0) : 0,
installed: Boolean(installedBundle),
installed_version: installedBundle ? PRESTOSQL_JDBC_DRIVER_VERSION : null,
update_available: false,
requires_java_runtime: true,
jre: "21",
jre_installed: true,
};
return managedJdbcDriverPaths(PRESTOSQL_MANAGED_JDBC_DRIVER, bundles);
}