feat(admin): support openGauss and Kingbase monitoring

This commit is contained in:
amwps290 2026-07-30 18:40:12 +08:00 committed by GitHub
parent cd3163d737
commit 51315e549f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 558 additions and 73 deletions

View File

@ -9,23 +9,7 @@ import { useConnectionStore } from "@/stores/connectionStore";
import MetricCard from "@/components/common/MetricCard.vue";
import MetricLineChart from "@/components/chart/MetricLineChart.vue";
import * as api from "@/lib/backend/api";
import {
computePgTps,
computeRate,
formatBytesPerSec,
formatNumber,
formatRate,
formatUptime,
isPgStatusCompatibilityError,
MAX_SAMPLES,
parsePgStatusRow,
pgCacheHitRatio,
PG_STATUS_LEGACY_SQL,
PG_STATUS_SQL,
PG_VARIABLES_SQL,
statusNumber,
type StatusSample,
} from "@/lib/database/postgresServerStatus";
import { computePgTps, computeRate, formatBytesPerSec, formatNumber, formatRate, formatUptime, MAX_SAMPLES, parsePgStatusRow, pgCacheHitRatio, resolveServerDashboardDriverForConnection, statusNumber, type StatusSample } from "@/lib/database/postgresServerStatus";
import { useVerticalOverlayScrollbar } from "@/composables/useVerticalOverlayScrollbar";
const props = defineProps<{
@ -58,7 +42,9 @@ const {
const fallbackStatusSql = ref<string | null>(null);
let refreshTimer: ReturnType<typeof setInterval> | null = null;
const connectionName = computed(() => connectionStore.getConfig(props.connectionId)?.name ?? "");
const connection = computed(() => connectionStore.getConfig(props.connectionId));
const statusDriver = computed(() => resolveServerDashboardDriverForConnection(connection.value));
const connectionName = computed(() => connection.value?.name ?? "");
const latest = computed(() => samples.value[samples.value.length - 1]);
const previous = computed(() => (samples.value.length >= 2 ? samples.value[samples.value.length - 2] : undefined));
@ -138,8 +124,10 @@ function formatClock(at: number): string {
}
async function fetchVariables() {
const activeDriver = statusDriver.value;
if (!activeDriver) return;
try {
const result = await api.executeQuery(props.connectionId, "", PG_VARIABLES_SQL, undefined, undefined, { maxRows: 2000 });
const result = await api.executeQuery(props.connectionId, "", activeDriver.variablesSql, undefined, undefined, { maxRows: 2000 });
variables.value = parsePgStatusRow(result);
} catch {
// Non-fatal: cards that depend on variables (max_connections/version) degrade.
@ -147,20 +135,22 @@ async function fetchVariables() {
}
async function fetchStatus(options: { silent?: boolean } = {}) {
const activeDriver = statusDriver.value;
if (!activeDriver) return;
if (fetching.value) return;
fetching.value = true;
if (!options.silent) loading.value = true;
error.value = "";
try {
await connectionStore.ensureConnected(props.connectionId);
const sql = fallbackStatusSql.value ?? PG_STATUS_SQL;
const sql = fallbackStatusSql.value ?? activeDriver.statusSql;
let result;
try {
result = await api.executeQuery(props.connectionId, "", sql, undefined, undefined, { maxRows: 2000 });
} catch (queryError) {
if (fallbackStatusSql.value || !isPgStatusCompatibilityError(queryError)) throw queryError;
result = await api.executeQuery(props.connectionId, "", PG_STATUS_LEGACY_SQL, undefined, undefined, { maxRows: 2000 });
fallbackStatusSql.value = PG_STATUS_LEGACY_SQL;
if (fallbackStatusSql.value || !activeDriver.fallbackStatusSql || !activeDriver.shouldUseFallbackStatusSql?.(queryError)) throw queryError;
result = await api.executeQuery(props.connectionId, "", activeDriver.fallbackStatusSql, undefined, undefined, { maxRows: 2000 });
fallbackStatusSql.value = activeDriver.fallbackStatusSql;
}
const sample: StatusSample = { at: Date.now(), status: parsePgStatusRow(result) };
const next = [...samples.value, sample];
@ -212,8 +202,8 @@ onUnmounted(stopAutoRefresh);
<div class="flex h-11 shrink-0 items-center gap-2 border-b bg-muted/20 px-3">
<Gauge class="h-4 w-4 text-primary" />
<div class="truncate text-sm font-semibold">{{ t("serverDashboard.title") }}</div>
<Badge variant="outline" class="h-5 rounded-md px-1.5 text-[11px]">{{ connectionName }}</Badge>
<Badge v-if="serverVersion" variant="secondary" class="h-5 rounded-md px-1.5 text-[11px]">{{ serverVersion }}</Badge>
<Badge variant="outline" class="h-5 max-w-48 truncate rounded-md px-1.5 text-[11px]" :title="connectionName">{{ connectionName }}</Badge>
<Badge v-if="serverVersion" variant="secondary" class="h-5 max-w-64 truncate rounded-md px-1.5 text-[11px]" :title="serverVersion">{{ serverVersion }}</Badge>
<div class="ml-auto flex items-center gap-2">
<span class="text-xs text-muted-foreground">{{ t("serverDashboard.autoRefresh") }}</span>
<Select :model-value="String(autoRefreshInterval)" @update:model-value="onIntervalChange">

View File

@ -99,7 +99,13 @@ async function load(options: { silent?: boolean } = {}) {
await connectionStore.ensureConnected(props.connection.id);
if (ownSessionId.value === null) {
try {
const idResult = await api.executeQuery(props.connection.id, "", activeDriver.ownSessionSql, undefined, undefined, { maxRows: 1 });
let idResult;
try {
idResult = await api.executeQuery(props.connection.id, "", activeDriver.ownSessionSql, undefined, undefined, { maxRows: 1 });
} catch (error) {
if (!activeDriver.fallbackOwnSessionSql || !activeDriver.shouldUseFallbackOwnSessionSql?.(error)) throw error;
idResult = await api.executeQuery(props.connection.id, "", activeDriver.fallbackOwnSessionSql, undefined, undefined, { maxRows: 1 });
}
const raw = idResult?.rows?.[0]?.[0];
const parsed = Number(raw);
if (Number.isFinite(parsed)) ownSessionId.value = parsed;
@ -143,17 +149,30 @@ async function confirmKill() {
killing.value = true;
try {
const killSql = activeDriver.buildKillSql(target.id);
let usedFallbackKillSql = false;
const executeKillSql = async (sql: string) => {
const results = await api.executeMulti(props.connection.id, "", sql, undefined, undefined, { maxRows: 1 });
const executionError = processListExecutionError(results);
if (executionError) throw new Error(executionError);
return results;
};
const result = await executeWithProductionSqlGuard({
connection: props.connection,
database: "",
sql: killSql,
source: t("production.sourceAdmin"),
execute: () => api.executeMulti(props.connection.id, "", killSql, undefined, undefined, { maxRows: 1 }),
execute: async () => {
try {
return await executeKillSql(killSql);
} catch (error) {
if (!activeDriver.buildFallbackKillSql || !activeDriver.shouldUseFallbackKillSql?.(error)) throw error;
usedFallbackKillSql = true;
return executeKillSql(activeDriver.buildFallbackKillSql(target.id));
}
},
});
if (result === undefined) return;
const executionError = processListExecutionError(result);
if (executionError) throw new Error(executionError);
const killResultError = activeDriver.killResultError?.(result);
const killResultError = usedFallbackKillSql ? activeDriver.fallbackKillResultError?.(result) : activeDriver.killResultError?.(result);
if (killResultError) throw new Error(killResultError);
toast(t("processList.killSuccess", { id: target.id }), 2500);
killTarget.value = null;

View File

@ -1138,7 +1138,7 @@ async function openServerDashboard() {
connectionStore.activeConnectionId = node.connectionId;
if (currentDatabaseType() === "nacos") {
queryStore.openNacosDashboard(node.connectionId);
} else if (currentDatabaseType() === "postgres") {
} else if (connectionSupportsPgServerDashboard(connectionStore.getConfig(node.connectionId))) {
queryStore.openPostgresDashboard(node.connectionId);
} else {
queryStore.openMysqlDashboard(node.connectionId);

View File

@ -1,6 +1,24 @@
import { describe, expect, it } from "vitest";
import type { QueryResult } from "@/types/database";
import { buildPgKillSql, isPgProcessListCompatibilityError, mapPgProcessRows, pgKillResultError, PG_PROCESS_LIST_LEGACY_SQL, PG_PROCESS_LIST_SQL, supportsPgProcessList } from "@/lib/database/postgresProcessList";
import {
buildKingbaseKillSql,
buildKingbasePgKillSql,
buildPgKillSql,
isKingbaseOwnSessionCatalogCompatibilityError,
isKingbaseProcessListCatalogCompatibilityError,
isKingbaseTerminateCatalogCompatibilityError,
isPgProcessListCompatibilityError,
KINGBASE_OWN_SESSION_SQL,
KINGBASE_PG_OWN_SESSION_SQL,
KINGBASE_PG_PROCESS_LIST_SQL,
KINGBASE_PROCESS_LIST_SQL,
mapPgProcessRows,
OPENGAUSS_OWN_SESSION_SQL,
OPENGAUSS_PROCESS_LIST_SQL,
pgKillResultError,
PG_PROCESS_LIST_LEGACY_SQL,
PG_PROCESS_LIST_SQL,
} from "@/lib/database/postgresProcessList";
import { connectionSupportsProcessList, resolveProcessListDriver, resolveProcessListDriverForConnection, supportsProcessList } from "@/lib/database/processListDrivers";
import type { ConnectionConfig } from "@/types/database";
@ -49,6 +67,55 @@ describe("buildPgKillSql", () => {
expect(() => buildPgKillSql(-1)).toThrow();
expect(() => buildPgKillSql(Number.NaN)).toThrow();
});
it("builds the KingbaseES sys_terminate_backend call with the same PID validation", () => {
expect(buildKingbaseKillSql(4211)).toBe("SELECT sys_terminate_backend(4211)");
expect(() => buildKingbaseKillSql(0)).toThrow();
expect(() => buildKingbaseKillSql(1.5)).toThrow();
});
});
describe("PostgreSQL-family process SQL", () => {
it("uses openGauss's pg catalog and boolean waiting column", () => {
const driver = resolveProcessListDriver("opengauss");
expect(driver?.listSql).toBe(OPENGAUSS_PROCESS_LIST_SQL);
expect(driver?.ownSessionSql).toBe(OPENGAUSS_OWN_SESSION_SQL);
expect(OPENGAUSS_PROCESS_LIST_SQL).toContain("FROM pg_catalog.pg_stat_activity");
expect(OPENGAUSS_PROCESS_LIST_SQL).toContain("CASE WHEN waiting");
expect(OPENGAUSS_PROCESS_LIST_SQL).not.toContain("wait_event_type");
expect(driver?.buildKillSql(7)).toBe("SELECT pg_terminate_backend(7)");
});
it("uses KingbaseES's sys catalog and session functions", () => {
const driver = resolveProcessListDriver("kingbase");
expect(driver?.listSql).toBe(KINGBASE_PROCESS_LIST_SQL);
expect(driver?.ownSessionSql).toBe(KINGBASE_OWN_SESSION_SQL);
expect(KINGBASE_PROCESS_LIST_SQL).toContain("FROM sys_catalog.sys_stat_activity");
expect(KINGBASE_PROCESS_LIST_SQL).toContain("wait_event_type");
expect(KINGBASE_PROCESS_LIST_SQL).toContain("extract(epoch FROM CAST(CURRENT_TIMESTAMP AS TIMESTAMP))");
expect(KINGBASE_PROCESS_LIST_SQL).toContain("extract(epoch FROM CAST(coalesce(query_start, xact_start, backend_start) AS TIMESTAMP))");
expect(KINGBASE_PROCESS_LIST_SQL).not.toContain("CURRENT_TIMESTAMP - coalesce(query_start");
expect(driver?.buildKillSql(7)).toBe("SELECT sys_terminate_backend(7)");
});
it("provides pg_catalog fallbacks for pg-compatible KingbaseES servers", () => {
const driver = resolveProcessListDriver("kingbase");
expect(driver?.fallbackListSql).toBe(KINGBASE_PG_PROCESS_LIST_SQL);
expect(driver?.fallbackOwnSessionSql).toBe(KINGBASE_PG_OWN_SESSION_SQL);
expect(driver?.buildFallbackKillSql?.(7)).toBe("SELECT pg_terminate_backend(7)");
expect(KINGBASE_PG_PROCESS_LIST_SQL).toContain("FROM pg_catalog.pg_stat_activity");
expect(KINGBASE_PG_PROCESS_LIST_SQL).not.toContain("sys_catalog");
expect(buildKingbasePgKillSql(4211)).toBe("SELECT pg_terminate_backend(4211)");
});
it("retries only missing sys relation/function errors", () => {
expect(isKingbaseProcessListCatalogCompatibilityError(new Error('relation "sys_catalog.sys_stat_activity" does not exist (SQLSTATE 42P01)'))).toBe(true);
expect(isKingbaseOwnSessionCatalogCompatibilityError(Object.assign(new Error("function sys_backend_pid() does not exist"), { code: "42883" }))).toBe(true);
expect(isKingbaseTerminateCatalogCompatibilityError(new Error("function sys_terminate_backend(integer) does not exist (SQLSTATE 42883)"))).toBe(true);
expect(isKingbaseProcessListCatalogCompatibilityError(Object.assign(new Error("permission denied for relation sys_catalog.sys_stat_activity"), { code: "42501" }))).toBe(false);
expect(isKingbaseOwnSessionCatalogCompatibilityError(new Error("authentication failed for sys_backend_pid"))).toBe(false);
expect(isKingbaseTerminateCatalogCompatibilityError(new Error("connection refused"))).toBe(false);
});
});
describe("Postgres compatibility", () => {
@ -69,19 +136,6 @@ describe("Postgres compatibility", () => {
});
});
describe("supportsPgProcessList", () => {
it("covers the Postgres-kernel family and excludes divergent wire-protocol engines", () => {
expect(supportsPgProcessList("postgres")).toBe(true);
// Postgres-kernel forks are unverified for now, so they stay excluded.
expect(supportsPgProcessList("opengauss")).toBe(false);
expect(supportsPgProcessList("kingbase")).toBe(false);
expect(supportsPgProcessList("redshift")).toBe(false);
expect(supportsPgProcessList("questdb")).toBe(false);
expect(supportsPgProcessList("mysql")).toBe(false);
expect(supportsPgProcessList(undefined)).toBe(false);
});
});
describe("resolveProcessListDriver", () => {
it("routes MySQL and Postgres engines to distinct drivers", () => {
const mysql = resolveProcessListDriver("mysql");
@ -97,6 +151,11 @@ describe("resolveProcessListDriver", () => {
it("unifies process-list support across both families", () => {
expect(supportsProcessList("mysql")).toBe(true);
expect(supportsProcessList("postgres")).toBe(true);
expect(supportsProcessList("opengauss")).toBe(true);
expect(supportsProcessList("kingbase")).toBe(true);
// Postgres wire-protocol lookalikes with divergent catalogs stay excluded.
expect(supportsProcessList("redshift")).toBe(false);
expect(supportsProcessList("questdb")).toBe(false);
expect(supportsProcessList("sqlite")).toBe(false);
expect(supportsProcessList(undefined)).toBe(false);
});
@ -110,6 +169,10 @@ describe("connectionSupportsProcessList", () => {
it("gates on the real connection profile", () => {
expect(connectionSupportsProcessList(conn({ db_type: "mysql" }))).toBe(true);
expect(connectionSupportsProcessList(conn({ db_type: "postgres" }))).toBe(true);
expect(connectionSupportsProcessList(conn({ db_type: "opengauss" }))).toBe(true);
expect(connectionSupportsProcessList(conn({ db_type: "gaussdb", driver_profile: "opengauss" }))).toBe(true);
expect(connectionSupportsProcessList(conn({ db_type: "gaussdb", driver_profile: "gaussdb" }))).toBe(false);
expect(connectionSupportsProcessList(conn({ db_type: "kingbase" }))).toBe(true);
expect(connectionSupportsProcessList(conn({ db_type: "sqlite" }))).toBe(false);
expect(connectionSupportsProcessList(undefined)).toBe(false);
});

View File

@ -7,13 +7,21 @@ import {
formatBytes,
formatBytesPerSec,
formatUptime,
isOpenGaussReplayRecordError,
isKingbaseStatusCatalogCompatibilityError,
isPgStatusCompatibilityError,
KINGBASE_PG_STATUS_SQL,
KINGBASE_STATUS_SQL,
KINGBASE_VARIABLES_SQL,
OPENGAUSS_STATUS_FALLBACK_SQL,
OPENGAUSS_STATUS_SQL,
OPENGAUSS_VARIABLES_SQL,
parsePgStatusRow,
pgCacheHitRatio,
PG_STATUS_LEGACY_SQL,
PG_STATUS_SQL,
resolveServerDashboardDriver,
statusNumber,
supportsServerDashboard,
type StatusSample,
} from "@/lib/database/postgresServerStatus";
@ -132,6 +140,50 @@ describe("PG_STATUS_LEGACY_SQL", () => {
});
});
describe("PostgreSQL-family status drivers", () => {
it("uses openGauss's pg catalog with xlog location functions", () => {
const driver = resolveServerDashboardDriver("opengauss");
expect(driver?.statusSql).toBe(OPENGAUSS_STATUS_SQL);
expect(driver?.variablesSql).toBe(OPENGAUSS_VARIABLES_SQL);
expect(OPENGAUSS_STATUS_SQL).toContain("FROM pg_catalog.pg_stat_database");
expect(OPENGAUSS_STATUS_SQL).toContain("FROM pg_catalog.pg_stat_activity");
expect(OPENGAUSS_STATUS_SQL).toContain("pg_current_xlog_location()");
expect(OPENGAUSS_STATUS_SQL).toContain("(pg_last_xlog_replay_location()).lsn");
expect(OPENGAUSS_STATUS_SQL).not.toContain("pg_xlog_location_diff(pg_last_xlog_replay_location()");
expect(OPENGAUSS_STATUS_SQL).not.toContain("pg_current_wal_lsn()");
// The record-notation replay form has a scalar-text fallback for builds whose
// pg_last_xlog_replay_location() returns text rather than a (term, lsn) record.
expect(driver?.fallbackStatusSql).toBe(OPENGAUSS_STATUS_FALLBACK_SQL);
expect(driver?.shouldUseFallbackStatusSql?.(new Error('could not identify column "lsn" in record data type'))).toBe(true);
expect(OPENGAUSS_STATUS_FALLBACK_SQL).toContain("CAST(pg_last_xlog_replay_location() AS text)");
expect(OPENGAUSS_STATUS_FALLBACK_SQL).toContain("pg_current_xlog_location()");
expect(OPENGAUSS_STATUS_FALLBACK_SQL).not.toContain(".lsn");
});
it("uses KingbaseES's sys catalog and sys backend functions", () => {
const driver = resolveServerDashboardDriver("kingbase");
expect(driver?.statusSql).toBe(KINGBASE_STATUS_SQL);
expect(driver?.variablesSql).toBe(KINGBASE_VARIABLES_SQL);
expect(KINGBASE_STATUS_SQL).toContain("FROM sys_catalog.sys_stat_database");
expect(KINGBASE_STATUS_SQL).toContain("FROM sys_catalog.sys_stat_activity");
expect(KINGBASE_STATUS_SQL).toContain("sys_backend_pid()");
expect(KINGBASE_STATUS_SQL).toContain("sys_current_wal_lsn()");
expect(KINGBASE_STATUS_SQL).toContain("extract(epoch FROM CAST(CURRENT_TIMESTAMP AS TIMESTAMP))");
expect(KINGBASE_STATUS_SQL).toContain("extract(epoch FROM CAST(sys_postmaster_start_time() AS TIMESTAMP))");
expect(KINGBASE_STATUS_SQL).not.toContain("CURRENT_TIMESTAMP - sys_postmaster_start_time()");
});
it("wires each engine-specific status fallback", () => {
// PostgreSQL falls back to the pre-PG10 xlog-named query; openGauss falls back
// to the scalar-text replay form; Kingbase falls back from sys_catalog/sys_*
// to pg_catalog/pg_* when the server only exposes PostgreSQL-compatible names.
expect(resolveServerDashboardDriver("postgres")?.fallbackStatusSql).toBe(PG_STATUS_LEGACY_SQL);
expect(resolveServerDashboardDriver("opengauss")?.fallbackStatusSql).toBe(OPENGAUSS_STATUS_FALLBACK_SQL);
expect(resolveServerDashboardDriver("kingbase")?.fallbackStatusSql).toBe(KINGBASE_PG_STATUS_SQL);
expect(resolveServerDashboardDriver("mysql")).toBeNull();
});
});
describe("isPgStatusCompatibilityError", () => {
it("detects the WAL-function-not-found message on servers without a code field", () => {
expect(isPgStatusCompatibilityError(new Error("function pg_current_wal_lsn() does not exist"))).toBe(true);
@ -157,17 +209,49 @@ describe("isPgStatusCompatibilityError", () => {
});
});
describe("supportsServerDashboard", () => {
it("is true for postgres only", () => {
expect(supportsServerDashboard("postgres")).toBe(true);
expect(supportsServerDashboard("mysql")).toBe(false);
expect(supportsServerDashboard("opengauss")).toBe(false);
expect(supportsServerDashboard("kingbase")).toBe(false);
expect(supportsServerDashboard(undefined)).toBe(false);
describe("isOpenGaussReplayRecordError", () => {
it("detects the .lsn-on-non-composite failure some openGauss builds raise", () => {
expect(isOpenGaussReplayRecordError(new Error('could not identify column "lsn" in record data type'))).toBe(true);
expect(isOpenGaussReplayRecordError(new Error("column notation .lsn applied to type text, which is not a composite type"))).toBe(true);
expect(isOpenGaussReplayRecordError(Object.assign(new Error("column notation .lsn applied to type text"), { code: "42809" }))).toBe(true);
});
it("does not misclassify unrelated errors", () => {
expect(isOpenGaussReplayRecordError(new Error("connection refused"))).toBe(false);
// Names an LSN function but is the pre-PG10 WAL-rename failure, not the record issue.
expect(isOpenGaussReplayRecordError(new Error("function pg_current_wal_lsn() does not exist"))).toBe(false);
});
});
describe("Kingbase catalog compatibility", () => {
it("keeps the sys_catalog query primary and provides a pg_catalog fallback", () => {
const driver = resolveServerDashboardDriver("kingbase");
expect(driver?.statusSql).toBe(KINGBASE_STATUS_SQL);
expect(driver?.fallbackStatusSql).toBe(KINGBASE_PG_STATUS_SQL);
expect(KINGBASE_STATUS_SQL).toContain("sys_catalog.sys_stat_database");
expect(KINGBASE_STATUS_SQL).toContain("sys_backend_pid()");
expect(KINGBASE_PG_STATUS_SQL).toContain("pg_catalog.pg_stat_database");
expect(KINGBASE_PG_STATUS_SQL).toContain("pg_backend_pid()");
expect(KINGBASE_PG_STATUS_SQL).not.toContain("sys_catalog");
expect(KINGBASE_PG_STATUS_SQL).not.toMatch(/\bsys_(?:backend|is|wal|last|current|postmaster)/);
});
it("falls back only for missing Kingbase sys catalog objects", () => {
expect(isKingbaseStatusCatalogCompatibilityError(Object.assign(new Error('relation "sys_catalog.sys_stat_database" does not exist'), { code: "42P01" }))).toBe(true);
expect(isKingbaseStatusCatalogCompatibilityError(new Error("function sys_current_wal_lsn() does not exist (SQLSTATE 42883)"))).toBe(true);
expect(isKingbaseStatusCatalogCompatibilityError(Object.assign(new Error("permission denied for relation sys_catalog.sys_stat_database"), { code: "42501" }))).toBe(false);
expect(isKingbaseStatusCatalogCompatibilityError(new Error("connection refused"))).toBe(false);
expect(isKingbaseStatusCatalogCompatibilityError(Object.assign(new Error('relation "other_table" does not exist'), { code: "42P01" }))).toBe(false);
});
});
describe("connectionSupportsServerDashboard", () => {
it("gates on the connection's effective db type", () => {
expect(connectionSupportsServerDashboard({ id: "pg", name: "Postgres", db_type: "postgres" } as any)).toBe(true);
expect(connectionSupportsServerDashboard({ id: "og", name: "openGauss", db_type: "opengauss" } as any)).toBe(true);
expect(connectionSupportsServerDashboard({ id: "og-import", name: "Imported openGauss", db_type: "gaussdb", driver_profile: "opengauss" } as any)).toBe(true);
expect(connectionSupportsServerDashboard({ id: "gauss", name: "GaussDB", db_type: "gaussdb", driver_profile: "gaussdb" } as any)).toBe(false);
expect(connectionSupportsServerDashboard({ id: "kb", name: "KingbaseES", db_type: "kingbase" } as any)).toBe(true);
expect(connectionSupportsServerDashboard({ id: "jdbc-pg", name: "JDBC Postgres", db_type: "jdbc", connection_string: "jdbc:postgresql://localhost/db" } as any)).toBe(true);
expect(connectionSupportsServerDashboard({ id: "mysql", name: "MySQL", db_type: "mysql" } as any)).toBe(false);
expect(connectionSupportsServerDashboard(undefined)).toBe(false);

View File

@ -0,0 +1,44 @@
type KingbaseCatalogObjectKind = "relation" | "function";
function errorCode(error: unknown): string {
if (typeof error !== "object" || error === null || !("code" in error)) return "";
return String((error as { code?: unknown }).code ?? "").toUpperCase();
}
function errorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
if (typeof error === "object" && error !== null && "message" in error) return String((error as { message?: unknown }).message ?? "");
return String(error);
}
function referencesCatalogObject(message: string, names: readonly string[]): boolean {
const normalized = message.toLowerCase();
return names.some((name) => {
const lowerName = name.toLowerCase();
const shortName = lowerName.slice(lowerName.lastIndexOf(".") + 1);
return normalized.includes(lowerName) || normalized.includes(shortName);
});
}
function isMissingKingbaseCatalogObject(error: unknown, kind: KingbaseCatalogObjectKind, names: readonly string[]): boolean {
const message = errorMessage(error);
if (!referencesCatalogObject(message, names)) return false;
const expectedCode = kind === "relation" ? "42P01" : "42883";
const directCode = errorCode(error);
if (directCode) return directCode === expectedCode;
const messageCode = message.match(/\b(?:SQLSTATE\s*[:=]?\s*)?(42P01|42883)\b/i)?.[1]?.toUpperCase();
if (messageCode) return messageCode === expectedCode;
const missingPattern = kind === "relation" ? /(?:relation|table)[\s\S]*?(?:does not exist|not found|undefined)|(?:does not exist|not found|undefined)[\s\S]*?(?:relation|table)/i : /function[\s\S]*?(?:does not exist|not found|undefined)|(?:does not exist|not found|undefined)[\s\S]*?function/i;
return missingPattern.test(message);
}
export function isMissingKingbaseSysRelation(error: unknown, names: readonly string[]): boolean {
return isMissingKingbaseCatalogObject(error, "relation", names);
}
export function isMissingKingbaseSysFunction(error: unknown, names: readonly string[]): boolean {
return isMissingKingbaseCatalogObject(error, "function", names);
}

View File

@ -1,4 +1,5 @@
import type { DatabaseType, QueryResult } from "@/types/database";
import type { QueryResult } from "@/types/database";
import { isMissingKingbaseSysFunction, isMissingKingbaseSysRelation } from "@/lib/database/kingbaseCatalogCompatibility";
/**
* PostgreSQL "current activity / process list" helpers. Pure and framework-free
@ -9,8 +10,6 @@ import type { DatabaseType, QueryResult } from "@/types/database";
* bits (coordinator, interval clamping, session counting) are shared from there.
*/
const PG_PROCESS_LIST_DB_TYPES = new Set<DatabaseType>(["postgres"]);
/**
* One row per server-side backend. `now() - query_start` gives the age of the
* currently running (or last) statement; we fall back to the transaction and
@ -42,8 +41,45 @@ export const PG_PROCESS_LIST_LEGACY_SQL = `SELECT pid,
FROM pg_stat_activity
ORDER BY time DESC NULLS LAST`;
/** openGauss exposes the legacy boolean `waiting` column rather than wait-event detail. */
export const OPENGAUSS_PROCESS_LIST_SQL = `SELECT pid,
usename AS "user",
datname AS db,
coalesce(host(client_addr), client_hostname, 'local') AS client,
application_name AS app,
state,
CASE WHEN waiting THEN 'Lock' ELSE '' END AS wait,
CAST(floor(extract(epoch FROM (CURRENT_TIMESTAMP - coalesce(query_start, xact_start, backend_start)))) AS BIGINT) AS time,
query
FROM pg_catalog.pg_stat_activity
ORDER BY time DESC NULLS LAST`;
/**
* KingbaseES uses sys_catalog. Numeric epoch subtraction works in both MySQL
* and Oracle modes despite their different datetime subtraction rules.
*/
export const KINGBASE_PROCESS_LIST_SQL = `SELECT pid,
usename AS "user",
datname AS db,
coalesce(CAST(client_addr AS VARCHAR), client_hostname, 'local') AS client,
application_name AS app,
state,
coalesce(nullif(concat_ws(':', wait_event_type, wait_event), ''), '') AS wait,
CAST(floor(
extract(epoch FROM CAST(CURRENT_TIMESTAMP AS TIMESTAMP))
- extract(epoch FROM CAST(coalesce(query_start, xact_start, backend_start) AS TIMESTAMP))
) AS BIGINT) AS time,
query
FROM sys_catalog.sys_stat_activity
ORDER BY time DESC NULLS LAST`;
export const KINGBASE_PG_PROCESS_LIST_SQL = KINGBASE_PROCESS_LIST_SQL.replace("sys_catalog.sys_stat_activity", "pg_catalog.pg_stat_activity");
/** Scalar query that returns the viewer's own backend pid. */
export const PG_OWN_SESSION_SQL = "SELECT pg_backend_pid()";
export const OPENGAUSS_OWN_SESSION_SQL = "SELECT pg_backend_pid()";
export const KINGBASE_OWN_SESSION_SQL = "SELECT sys_backend_pid()";
export const KINGBASE_PG_OWN_SESSION_SQL = "SELECT pg_backend_pid()";
export interface PgProcessRow {
id: number;
@ -117,19 +153,45 @@ export function mapPgProcessRows(result: QueryResult | null | undefined): PgProc
* finite positive integer (never interpolated as free text) so there is no
* injection path.
*/
export function buildPgKillSql(pid: number): string {
function validateBackendPid(pid: number): void {
if (!Number.isInteger(pid) || pid <= 0) {
throw new Error(`Invalid backend pid: ${pid}`);
}
}
export function buildPgKillSql(pid: number): string {
validateBackendPid(pid);
return `SELECT pg_terminate_backend(${pid})`;
}
export function buildKingbaseKillSql(pid: number): string {
validateBackendPid(pid);
return `SELECT sys_terminate_backend(${pid})`;
}
export function buildKingbasePgKillSql(pid: number): string {
validateBackendPid(pid);
return `SELECT pg_terminate_backend(${pid})`;
}
/** Return an error when PostgreSQL declines to terminate the target backend. */
export function pgKillResultError(results: QueryResult[]): string | null {
return backendKillResultError(results, "pg_terminate_backend");
}
export function kingbaseKillResultError(results: QueryResult[]): string | null {
return backendKillResultError(results, "sys_terminate_backend");
}
export function kingbasePgKillResultError(results: QueryResult[]): string | null {
return backendKillResultError(results, "pg_terminate_backend");
}
function backendKillResultError(results: QueryResult[], functionName: string): string | null {
const result = results.find((item) => item.execution_error !== true);
const value = result?.rows?.[0]?.[0];
if (value === true || value === 1 || String(value).toLowerCase() === "t" || String(value).toLowerCase() === "true") return null;
return "pg_terminate_backend did not terminate the backend";
return `${functionName} did not terminate the backend`;
}
/** Detect the undefined-column failure produced by pre-9.6 pg_stat_activity. */
@ -140,7 +202,14 @@ export function isPgProcessListCompatibilityError(error: unknown): boolean {
return /(?:wait_event_type|wait_event).*(?:does not exist|42703)|(?:does not exist|42703).*(?:wait_event_type|wait_event)/i.test(message);
}
/** Whether the given database type exposes the Postgres process-list viewer. */
export function supportsPgProcessList(dbType: DatabaseType | undefined): boolean {
return !!dbType && PG_PROCESS_LIST_DB_TYPES.has(dbType);
export function isKingbaseProcessListCatalogCompatibilityError(error: unknown): boolean {
return isMissingKingbaseSysRelation(error, ["sys_catalog.sys_stat_activity"]);
}
export function isKingbaseOwnSessionCatalogCompatibilityError(error: unknown): boolean {
return isMissingKingbaseSysFunction(error, ["sys_backend_pid"]);
}
export function isKingbaseTerminateCatalogCompatibilityError(error: unknown): boolean {
return isMissingKingbaseSysFunction(error, ["sys_terminate_backend"]);
}

View File

@ -1,5 +1,6 @@
import type { ConnectionConfig, DatabaseType, QueryResult } from "@/types/database";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { isMissingKingbaseSysFunction, isMissingKingbaseSysRelation } from "@/lib/database/kingbaseCatalogCompatibility";
import { computeRate, formatBytes, formatBytesPerSec, formatNumber, formatRate, formatUptime, statusEntries, statusNumber, type StatusEntry, type StatusMap, type StatusSample } from "@/lib/database/serverMetrics";
/**
@ -92,12 +93,113 @@ export const PG_STATUS_LEGACY_SQL = PG_STATUS_SQL.replace(/\bpg_current_wal_lsn\
export const PG_VARIABLES_SQL = "SELECT current_setting('max_connections') AS max_connections, current_setting('server_version') AS version";
/**
* openGauss keeps the xlog names. Its current-location function returns text,
* while the replay-location function returns a `(term, lsn)` record.
*/
export const OPENGAUSS_STATUS_SQL = `WITH db_stats AS (
SELECT
coalesce(sum(xact_commit),0) AS xact_commit,
coalesce(sum(xact_rollback),0) AS xact_rollback,
coalesce(sum(blks_hit),0) AS blks_hit,
coalesce(sum(blks_read),0) AS blks_read,
coalesce(sum(tup_returned),0) AS tup_returned,
coalesce(sum(tup_fetched),0) AS tup_fetched,
coalesce(sum(tup_inserted),0) AS tup_inserted,
coalesce(sum(tup_updated),0) AS tup_updated,
coalesce(sum(tup_deleted),0) AS tup_deleted,
coalesce(sum(deadlocks),0) AS deadlocks,
coalesce(sum(temp_files),0) AS temp_files
FROM pg_catalog.pg_stat_database
), activity_stats AS (
SELECT
coalesce(sum(CASE WHEN state IS NOT NULL THEN 1 ELSE 0 END),0) AS connections,
coalesce(sum(CASE WHEN state = 'active' THEN 1 ELSE 0 END),0) AS active_connections,
coalesce(sum(CASE WHEN state = 'idle' THEN 1 ELSE 0 END),0) AS idle_connections
FROM pg_catalog.pg_stat_activity
WHERE pid <> pg_backend_pid()
)
SELECT
db_stats.*,
activity_stats.*,
coalesce(CASE WHEN pg_is_in_recovery()
THEN pg_xlog_location_diff(CAST((pg_last_xlog_replay_location()).lsn AS text), CAST('0/0' AS text))
ELSE pg_xlog_location_diff(CAST(pg_current_xlog_location() AS text), CAST('0/0' AS text))
END, 0) AS wal_bytes,
CAST(floor(extract(epoch FROM (CURRENT_TIMESTAMP - pg_postmaster_start_time()))) AS BIGINT) AS uptime_seconds
FROM db_stats
CROSS JOIN activity_stats`;
/**
* Some openGauss builds return `text` (not a `(term, lsn)` record) from
* `pg_last_xlog_replay_location()`, which makes the `.lsn` field access in
* `OPENGAUSS_STATUS_SQL` fail to *parse* and both CASE branches are planned,
* so it fails even on a primary. This fallback drops the record notation and
* reads the replay location as scalar text; the driver retries with it only
* after the primary query raises `isOpenGaussReplayRecordError`.
*/
export const OPENGAUSS_STATUS_FALLBACK_SQL = OPENGAUSS_STATUS_SQL.replace("(pg_last_xlog_replay_location()).lsn", "pg_last_xlog_replay_location()");
export const OPENGAUSS_VARIABLES_SQL = "SELECT current_setting('max_connections') AS max_connections, version() AS version";
/**
* KingbaseES exposes monitoring under sys_* names. Epoch values are subtracted
* numerically because MySQL mode types CURRENT_TIMESTAMP as datetime while the
* monitoring functions return timestamp with time zone.
*/
function buildKingbaseStatusSql(catalog: "sys_catalog" | "pg_catalog", prefix: "sys" | "pg"): string {
return `WITH db_stats AS (
SELECT
coalesce(sum(xact_commit),0) AS xact_commit,
coalesce(sum(xact_rollback),0) AS xact_rollback,
coalesce(sum(blks_hit),0) AS blks_hit,
coalesce(sum(blks_read),0) AS blks_read,
coalesce(sum(tup_returned),0) AS tup_returned,
coalesce(sum(tup_fetched),0) AS tup_fetched,
coalesce(sum(tup_inserted),0) AS tup_inserted,
coalesce(sum(tup_updated),0) AS tup_updated,
coalesce(sum(tup_deleted),0) AS tup_deleted,
coalesce(sum(deadlocks),0) AS deadlocks,
coalesce(sum(temp_files),0) AS temp_files
FROM ${catalog}.${prefix}_stat_database
), activity_stats AS (
SELECT
coalesce(sum(CASE WHEN state IS NOT NULL THEN 1 ELSE 0 END),0) AS connections,
coalesce(sum(CASE WHEN state = 'active' THEN 1 ELSE 0 END),0) AS active_connections,
coalesce(sum(CASE WHEN state = 'idle' THEN 1 ELSE 0 END),0) AS idle_connections
FROM ${catalog}.${prefix}_stat_activity
WHERE pid <> ${prefix}_backend_pid()
)
SELECT
db_stats.*,
activity_stats.*,
coalesce(CASE WHEN ${prefix}_is_in_recovery()
THEN ${prefix}_wal_lsn_diff(${prefix}_last_wal_replay_lsn(), '0/0')
ELSE ${prefix}_wal_lsn_diff(${prefix}_current_wal_lsn(), '0/0')
END, 0) AS wal_bytes,
CAST(floor(
extract(epoch FROM CAST(CURRENT_TIMESTAMP AS TIMESTAMP))
- extract(epoch FROM CAST(${prefix}_postmaster_start_time() AS TIMESTAMP))
) AS BIGINT) AS uptime_seconds
FROM db_stats
CROSS JOIN activity_stats`;
}
export const KINGBASE_STATUS_SQL = buildKingbaseStatusSql("sys_catalog", "sys");
export const KINGBASE_PG_STATUS_SQL = buildKingbaseStatusSql("pg_catalog", "pg");
export const KINGBASE_VARIABLES_SQL = "SELECT current_setting('max_connections') AS max_connections, version() AS version";
export interface PgServerStatusDriver {
statusSql: string;
variablesSql: string;
fallbackStatusSql?: string;
shouldUseFallbackStatusSql?(error: unknown): boolean;
}
/** Max samples retained for the live charts (~ a few minutes at 5s cadence). */
export const MAX_SAMPLES = 60;
/** Engines exposing `pg_stat_database`/`pg_stat_activity` in the shape this dashboard expects. */
const SERVER_DASHBOARD_DB_TYPES = new Set<DatabaseType>(["postgres"]);
/** Detect the undefined-function failure produced by pre-10 servers lacking `pg_current_wal_lsn`/`pg_last_wal_replay_lsn`/`pg_wal_lsn_diff`. */
export function isPgStatusCompatibilityError(error: unknown): boolean {
const code = typeof error === "object" && error !== null && "code" in error ? String((error as { code?: unknown }).code ?? "") : "";
@ -111,6 +213,61 @@ export function isPgStatusCompatibilityError(error: unknown): boolean {
return /(?:pg_current_wal_lsn|pg_last_wal_replay_lsn|pg_wal_lsn_diff)/i.test(message) && /does not exist/i.test(message);
}
/**
* Detect the failure raised when an openGauss build's `pg_last_xlog_replay_location()`
* returns `text` instead of a `(term, lsn)` record, so the `.lsn` field access in
* `OPENGAUSS_STATUS_SQL` is invalid. SQLSTATE 42809 is `wrong_object_type` (column
* notation on a non-composite value); some builds report it as 42703 with an
* "identify column" message instead. Gates the retry with `OPENGAUSS_STATUS_FALLBACK_SQL`.
* NOTE: confirm the exact message/SQLSTATE on the target openGauss build.
*/
export function isOpenGaussReplayRecordError(error: unknown): boolean {
const code = typeof error === "object" && error !== null && "code" in error ? String((error as { code?: unknown }).code ?? "") : "";
if (code === "42809") return true;
const message = error instanceof Error ? error.message : String(error);
return /\blsn\b/i.test(message) && /(?:composite|record data type|column notation|identify column)/i.test(message);
}
export function isKingbaseStatusCatalogCompatibilityError(error: unknown): boolean {
return isMissingKingbaseSysRelation(error, ["sys_catalog.sys_stat_database", "sys_catalog.sys_stat_activity"]) || isMissingKingbaseSysFunction(error, ["sys_backend_pid", "sys_is_in_recovery", "sys_wal_lsn_diff", "sys_last_wal_replay_lsn", "sys_current_wal_lsn", "sys_postmaster_start_time"]);
}
const POSTGRES_STATUS_DRIVER: PgServerStatusDriver = {
statusSql: PG_STATUS_SQL,
variablesSql: PG_VARIABLES_SQL,
fallbackStatusSql: PG_STATUS_LEGACY_SQL,
shouldUseFallbackStatusSql: isPgStatusCompatibilityError,
};
const OPENGAUSS_STATUS_DRIVER: PgServerStatusDriver = {
statusSql: OPENGAUSS_STATUS_SQL,
variablesSql: OPENGAUSS_VARIABLES_SQL,
fallbackStatusSql: OPENGAUSS_STATUS_FALLBACK_SQL,
shouldUseFallbackStatusSql: isOpenGaussReplayRecordError,
};
const KINGBASE_STATUS_DRIVER: PgServerStatusDriver = {
statusSql: KINGBASE_STATUS_SQL,
variablesSql: KINGBASE_VARIABLES_SQL,
fallbackStatusSql: KINGBASE_PG_STATUS_SQL,
shouldUseFallbackStatusSql: isKingbaseStatusCatalogCompatibilityError,
};
/** Resolve the SQL contract used by the shared PostgreSQL-family dashboard. */
export function resolveServerDashboardDriver(dbType: DatabaseType | undefined): PgServerStatusDriver | null {
if (dbType === "postgres") return POSTGRES_STATUS_DRIVER;
if (dbType === "opengauss") return OPENGAUSS_STATUS_DRIVER;
if (dbType === "kingbase") return KINGBASE_STATUS_DRIVER;
return null;
}
export function resolveServerDashboardDriverForConnection(connection: ConnectionConfig | undefined): PgServerStatusDriver | null {
if (!connection) return null;
const dbType = effectiveDatabaseTypeForConnection(connection);
if (dbType === "gaussdb" && connection.driver_profile?.toLowerCase() === "opengauss") return OPENGAUSS_STATUS_DRIVER;
return resolveServerDashboardDriver(dbType);
}
/** Parse the single-row `PG_STATUS_SQL` / `PG_VARIABLES_SQL` result into a name/value map. */
export function parsePgStatusRow(result: QueryResult | null | undefined): StatusMap {
const map: StatusMap = {};
@ -142,12 +299,7 @@ export function pgCacheHitRatio(status: StatusMap): number | null {
return Math.max(0, Math.min(100, ratio));
}
/** Whether the given database type exposes the Postgres server dashboard. */
export function supportsServerDashboard(dbType: DatabaseType | undefined): boolean {
return !!dbType && SERVER_DASHBOARD_DB_TYPES.has(dbType);
}
/** Connection-aware gate (mirrors the MySQL server-dashboard gate). */
export function connectionSupportsServerDashboard(connection: ConnectionConfig | undefined): boolean {
return !!connection && supportsServerDashboard(effectiveDatabaseTypeForConnection(connection));
return resolveServerDashboardDriverForConnection(connection) !== null;
}

View File

@ -1,7 +1,28 @@
import type { ConnectionConfig, DatabaseType, QueryResult } from "@/types/database";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { buildKillSql as buildMysqlKillSql, mapProcessRows as mapMysqlProcessRows, PROCESS_LIST_SQL as MYSQL_PROCESS_LIST_SQL, supportsProcessList as supportsMysqlProcessList } from "./mysqlProcessList";
import { buildPgKillSql, isPgProcessListCompatibilityError, mapPgProcessRows, pgKillResultError, PG_OWN_SESSION_SQL, PG_PROCESS_LIST_LEGACY_SQL, PG_PROCESS_LIST_SQL, supportsPgProcessList } from "./postgresProcessList";
import {
buildKingbaseKillSql,
buildKingbasePgKillSql,
buildPgKillSql,
isKingbaseOwnSessionCatalogCompatibilityError,
isKingbaseProcessListCatalogCompatibilityError,
isKingbaseTerminateCatalogCompatibilityError,
isPgProcessListCompatibilityError,
kingbaseKillResultError,
kingbasePgKillResultError,
KINGBASE_OWN_SESSION_SQL,
KINGBASE_PG_OWN_SESSION_SQL,
KINGBASE_PG_PROCESS_LIST_SQL,
KINGBASE_PROCESS_LIST_SQL,
mapPgProcessRows,
OPENGAUSS_OWN_SESSION_SQL,
OPENGAUSS_PROCESS_LIST_SQL,
pgKillResultError,
PG_OWN_SESSION_SQL,
PG_PROCESS_LIST_LEGACY_SQL,
PG_PROCESS_LIST_SQL,
} from "./postgresProcessList";
/**
* Engine-agnostic process-list model. Each supported engine contributes a driver
@ -34,6 +55,10 @@ export interface ProcessListDriver {
shouldUseFallbackListSql?(error: unknown): boolean;
/** Scalar SQL returning the caller's own session id (nullable path tolerated). */
ownSessionSql: string;
/** Compatibility query used when the primary own-session function is unavailable. */
fallbackOwnSessionSql?: string;
/** Restrict own-session fallback attempts to known compatibility failures. */
shouldUseFallbackOwnSessionSql?(error: unknown): boolean;
/** Columns to render, in display order. */
columns: ProcessColumn[];
/** Column key used for the initial sort. */
@ -44,8 +69,14 @@ export interface ProcessListDriver {
mapRows(result: QueryResult | null | undefined): ProcessRow[];
/** Build the validated statement that kills the given session id. */
buildKillSql(id: number): string;
/** Build the compatibility statement used when the primary kill function is unavailable. */
buildFallbackKillSql?(id: number): string;
/** Restrict kill fallback attempts to known compatibility failures. */
shouldUseFallbackKillSql?(error: unknown): boolean;
/** Validate any engine-specific success value returned by the kill statement. */
killResultError?(results: QueryResult[]): string | null;
/** Validate the success value returned by the compatibility kill statement. */
fallbackKillResultError?(results: QueryResult[]): string | null;
}
const MYSQL_COLUMNS: ProcessColumn[] = [
@ -95,10 +126,41 @@ const POSTGRES_DRIVER: ProcessListDriver = {
killResultError: pgKillResultError,
};
const OPENGAUSS_DRIVER: ProcessListDriver = {
listSql: OPENGAUSS_PROCESS_LIST_SQL,
ownSessionSql: OPENGAUSS_OWN_SESSION_SQL,
columns: POSTGRES_COLUMNS,
defaultSortKey: "time",
maxRows: 5000,
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
buildKillSql: buildPgKillSql,
killResultError: pgKillResultError,
};
const KINGBASE_DRIVER: ProcessListDriver = {
listSql: KINGBASE_PROCESS_LIST_SQL,
fallbackListSql: KINGBASE_PG_PROCESS_LIST_SQL,
shouldUseFallbackListSql: isKingbaseProcessListCatalogCompatibilityError,
ownSessionSql: KINGBASE_OWN_SESSION_SQL,
fallbackOwnSessionSql: KINGBASE_PG_OWN_SESSION_SQL,
shouldUseFallbackOwnSessionSql: isKingbaseOwnSessionCatalogCompatibilityError,
columns: POSTGRES_COLUMNS,
defaultSortKey: "time",
maxRows: 5000,
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
buildKillSql: buildKingbaseKillSql,
buildFallbackKillSql: buildKingbasePgKillSql,
shouldUseFallbackKillSql: isKingbaseTerminateCatalogCompatibilityError,
killResultError: kingbaseKillResultError,
fallbackKillResultError: kingbasePgKillResultError,
};
/** Resolve the process-list driver for a connection, or null if unsupported. */
export function resolveProcessListDriver(dbType: DatabaseType | undefined): ProcessListDriver | null {
if (supportsMysqlProcessList(dbType)) return MYSQL_DRIVER;
if (supportsPgProcessList(dbType)) return POSTGRES_DRIVER;
if (dbType === "postgres") return POSTGRES_DRIVER;
if (dbType === "opengauss") return OPENGAUSS_DRIVER;
if (dbType === "kingbase") return KINGBASE_DRIVER;
return null;
}
@ -124,7 +186,9 @@ export function resolveProcessListDriverForConnection(connection: ConnectionConf
const profile = [connection.driver_profile, connection.connection_string, connection.jdbc_driver_class, ...(connection.jdbc_driver_paths ?? [])].filter(Boolean).join("\n");
if (MYSQL_LOOKALIKE_JDBC.test(profile)) return null;
}
return resolveProcessListDriver(effectiveDatabaseTypeForConnection(connection));
const dbType = effectiveDatabaseTypeForConnection(connection);
if (dbType === "gaussdb" && connection.driver_profile?.toLowerCase() === "opengauss") return OPENGAUSS_DRIVER;
return resolveProcessListDriver(dbType);
}
/** Connection-aware process-list gate (mirrors the server-dashboard gate). */