feat(jdbc): add managed Apache Phoenix profile
This commit is contained in:
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");
|
||||
|
||||
+5
-3
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user