feat(admin): support openGauss and Kingbase monitoring
This commit is contained in:
parent
cd3163d737
commit
51315e549f
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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"]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
Loading…
Reference in New Issue