feat(mysql): terminate sessions from the process list

This commit is contained in:
zipg
2026-09-20 19:35:57 +08:00
committed by GitHub
parent 6aab94b3a8
commit 04c55ad067
21 changed files with 312 additions and 18 deletions
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Activity, AlertTriangle, ArrowDown, ArrowUp, Ban, Copy, Loader2, RefreshCcw, Search } from "@lucide/vue";
import { Activity, AlertTriangle, ArrowDown, ArrowUp, Ban, Copy, Loader2, PlugZap, RefreshCcw, Search } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
@@ -54,14 +54,18 @@ let timer: ReturnType<typeof setInterval> | undefined;
const cancelTarget = ref<ProcessRow | null>(null);
const canceling = ref(false);
const terminateTarget = ref<ProcessRow | null>(null);
const terminating = ref(false);
const batchSupported = computed(() => driver.value?.supportsBatchCancel === true);
// Engines without a terminate statement only expose query cancellation.
const terminateSupported = computed(() => typeof driver.value?.buildTerminateSessionSql === "function");
const selectedIds = ref(new Set<number>());
// Anchor row for Shift+click range selection, in the current display order.
const selectionAnchorId = ref<number | null>(null);
const batchTargets = ref<ProcessRow[] | null>(null);
const batchResult = ref<{ succeeded: number; failures: { id: number; message: string }[] } | null>(null);
const refreshing = ref(false);
const actionsLocked = computed(() => canceling.value || cancelTarget.value !== null || batchTargets.value !== null);
const actionsLocked = computed(() => canceling.value || cancelTarget.value !== null || terminating.value || terminateTarget.value !== null || batchTargets.value !== null);
let connectionGeneration = 0;
let disposed = false;
const fallbackListSql = ref<string | null>(null);
@@ -307,6 +311,62 @@ async function confirmCancel() {
}
}
function requestTerminate(row: ProcessRow) {
if (isOwnSession(row) || actionsLocked.value || refreshing.value || !terminateSupported.value) return;
terminateTarget.value = row;
}
/**
* Close the session itself. `KILL QUERY` / `pg_cancel_backend` leave an idle session
* connected, so an idle row could never be removed from the list before this action.
*/
async function confirmTerminate() {
const target = terminateTarget.value;
const activeDriver = driver.value;
const buildTerminateSql = activeDriver?.buildTerminateSessionSql;
if (!target || !activeDriver || !buildTerminateSql || terminating.value) return;
const connection = { ...props.connection };
const generation = connectionGeneration;
const isCurrent = () => !disposed && generation === connectionGeneration;
terminating.value = true;
try {
const terminateSql = buildTerminateSql.call(activeDriver, target.id);
let usedFallbackTerminateSql = false;
const executeTerminateSql = async (sql: string) => {
if (!isCurrent()) return undefined;
const results = await api.executeMulti(connection.id, "", sql, undefined, undefined, { maxRows: 1 });
const executionError = processListExecutionError(results);
if (executionError) throw new Error(executionError);
return results;
};
const result = await executeWithProductionSqlGuard({
connection,
database: "",
sql: terminateSql,
source: t("production.sourceAdmin"),
execute: async () => {
try {
return await executeTerminateSql(terminateSql);
} catch (error) {
if (!activeDriver.buildFallbackTerminateSessionSql || !activeDriver.shouldUseFallbackTerminateSessionSql?.(error)) throw error;
usedFallbackTerminateSql = true;
return executeTerminateSql(activeDriver.buildFallbackTerminateSessionSql(target.id));
}
},
});
if (result === undefined || !isCurrent()) return;
const terminateResultError = usedFallbackTerminateSql ? activeDriver.fallbackTerminateSessionResultError?.(result) : activeDriver.terminateSessionResultError?.(result);
if (terminateResultError) throw new Error(terminateResultError);
toast(t("processList.terminateSuccess", { id: target.id }), 2500);
terminateTarget.value = null;
} catch (error: any) {
if (isCurrent()) toast(t("processList.terminateFailed", { message: error?.message || String(error) }), 5000);
} finally {
terminating.value = false;
if (terminateTarget.value === null) void load({ silent: true });
}
}
function stopTimer() {
if (timer) {
clearInterval(timer);
@@ -433,7 +493,7 @@ onBeforeUnmount(() => {
<ArrowDown v-else-if="sortKey === column.key && sortDir === 'desc'" class="h-3 w-3" />
</span>
</th>
<th class="w-16 whitespace-nowrap border-b px-3 py-2 text-right font-medium">{{ t("processList.colActions") }}</th>
<th class="w-40 whitespace-nowrap border-b px-3 py-2 text-right font-medium">{{ t("processList.colActions") }}</th>
</tr>
</thead>
<tbody>
@@ -463,17 +523,31 @@ onBeforeUnmount(() => {
<template v-else>{{ row[column.key] === null || row[column.key] === undefined ? "—" : row[column.key] }}</template>
</td>
<td class="px-3 py-1.5 text-right">
<Button
variant="ghost"
size="sm"
class="h-6 gap-1 px-1.5 text-[11px] text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
:disabled="isOwnSession(row) || actionsLocked || refreshing"
:title="isOwnSession(row) ? t('processList.cannotKillSelf') : t('processList.kill')"
@click="requestCancel(row)"
>
<Ban class="h-3.5 w-3.5" />
{{ t("processList.kill") }}
</Button>
<span class="inline-flex items-center justify-end gap-1">
<Button
variant="ghost"
size="sm"
class="h-6 gap-1 px-1.5 text-[11px] text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
:disabled="isOwnSession(row) || actionsLocked || refreshing"
:title="isOwnSession(row) ? t('processList.cannotKillSelf') : t('processList.kill')"
@click="requestCancel(row)"
>
<Ban class="h-3.5 w-3.5" />
{{ t("processList.kill") }}
</Button>
<Button
v-if="terminateSupported"
variant="ghost"
size="sm"
class="h-6 gap-1 px-1.5 text-[11px] text-destructive hover:bg-destructive/10 hover:text-destructive disabled:opacity-40"
:disabled="isOwnSession(row) || actionsLocked || refreshing"
:title="isOwnSession(row) ? t('processList.cannotTerminateSelf') : t('processList.terminate')"
@click="requestTerminate(row)"
>
<PlugZap class="h-3.5 w-3.5" />
{{ t("processList.terminate") }}
</Button>
</span>
</td>
</tr>
<tr v-if="!loading && filteredRows.length === 0">
@@ -513,6 +587,34 @@ onBeforeUnmount(() => {
</DialogContent>
</Dialog>
<Dialog
:open="terminateTarget !== null"
@update:open="
(open) => {
if (!open && !terminating) terminateTarget = null;
}
"
>
<DialogContent class="max-w-sm" :show-close-button="!terminating">
<DialogHeader>
<DialogTitle class="flex items-center gap-2">
<AlertTriangle class="h-4 w-4 text-destructive" />
{{ t("processList.terminateTitle") }}
</DialogTitle>
</DialogHeader>
<p v-if="terminateTarget" class="text-sm text-muted-foreground">
{{ t("processList.terminateConfirm", { id: terminateTarget.id, user: terminateTarget.user }) }}
</p>
<DialogFooter>
<Button variant="outline" :disabled="terminating" @click="terminateTarget = null">{{ t("dangerDialog.cancel") }}</Button>
<Button variant="destructive" :disabled="terminating" @click="confirmTerminate">
<Loader2 v-if="terminating" class="mr-1.5 h-3.5 w-3.5 animate-spin" />
{{ t("processList.terminate") }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
:open="batchTargets !== null"
@update:open="
+6
View File
@@ -3850,6 +3850,12 @@ export default withEnglishFallback({
killConfirm: "{id} ({user}) sessiyasında icra olunan sorğu ləğv edilsin? Sessiyanın əlaqəsi saxlanacaq.",
killSuccess: "{id} sessiyasında icra olunan sorğu ləğv edildi",
killFailed: "Sorğunu ləğv etmək mümkün olmadı: {message}",
terminate: "Sessiyanı sonlandır",
cannotTerminateSelf: "Öz sessiyanızı sonlandıra bilməzsiniz",
terminateTitle: "Sessiyanı sonlandır",
terminateConfirm: "{id} ({user}) sessiyası sonlandırılsın? Əlaqə dərhal bağlanır və təsdiqlənməmiş tranzaksiyalar geri alınır.",
terminateSuccess: "{id} sessiyası sonlandırıldı",
terminateFailed: "Sessiyanı sonlandırmaq mümkün olmadı: {message}",
previewTitle: "İfadə",
copy: "Kopyala",
copied: "Kopyalandı",
+6
View File
@@ -4048,6 +4048,12 @@ export default {
killConfirm: "Cancel the running query for session {id} ({user})? The session will remain connected.",
killSuccess: "Canceled the running query for session {id}",
killFailed: "Failed to cancel query: {message}",
terminate: "Terminate session",
cannotTerminateSelf: "You cannot terminate your own session",
terminateTitle: "Terminate session",
terminateConfirm: "Terminate session {id} ({user})? The connection is closed immediately and any uncommitted transaction is rolled back.",
terminateSuccess: "Terminated session {id}",
terminateFailed: "Failed to terminate session: {message}",
previewTitle: "Statement",
copy: "Copy",
copied: "Copied",
+6
View File
@@ -9538,6 +9538,12 @@ export default withEnglishFallback({
killConfirm: "¿Cancelar la consulta en ejecución de la sesión {id} ({user})? La sesión permanecerá conectada.",
killSuccess: "Consulta en ejecución de la sesión {id} cancelada",
killFailed: "Error al cancelar la consulta: {message}",
terminate: "Terminar sesión",
cannotTerminateSelf: "No se puede terminar la sesión actual",
terminateTitle: "Terminar sesión",
terminateConfirm: "¿Terminar la sesión {id} ({user})? La conexión se cierra de inmediato y cualquier transacción sin confirmar se revierte.",
terminateSuccess: "Sesión {id} terminada",
terminateFailed: "Error al terminar la sesión: {message}",
colPid: "PID",
colClient: "Cliente",
colApp: "Aplicación",
+6
View File
@@ -9538,6 +9538,12 @@ export default withEnglishFallback({
killConfirm: "Annullare la query in esecuzione della sessione {id} ({user})? La sessione resterà connessa.",
killSuccess: "Query in esecuzione della sessione {id} annullata",
killFailed: "Impossibile annullare la query: {message}",
terminate: "Termina sessione",
cannotTerminateSelf: "Impossibile terminare la sessione corrente",
terminateTitle: "Termina sessione",
terminateConfirm: "Terminare la sessione {id} ({user})? La connessione viene chiusa immediatamente e le transazioni non confermate vengono annullate.",
terminateSuccess: "Sessione {id} terminata",
terminateFailed: "Impossibile terminare la sessione: {message}",
colPid: "PID",
colClient: "Client",
colApp: "Applicazione",
+6
View File
@@ -9588,6 +9588,12 @@ export default withEnglishFallback({
killConfirm: "セッション {id}({user})で実行中のクエリをキャンセルしますか?セッションの接続は維持されます。",
killSuccess: "セッション {id} で実行中のクエリをキャンセルしました。",
killFailed: "クエリのキャンセルに失敗しました:{message}",
terminate: "セッションを終了",
cannotTerminateSelf: "現在のセッションは終了できません",
terminateTitle: "セッションを終了",
terminateConfirm: "セッション {id}({user})を終了しますか?接続は直ちに切断され、未コミットのトランザクションはロールバックされます。",
terminateSuccess: "セッション {id} を終了しました。",
terminateFailed: "セッションの終了に失敗しました:{message}",
colPid: "PID",
colClient: "クライアント",
colApp: "アプリケーション",
+6
View File
@@ -3812,6 +3812,12 @@ export default withEnglishFallback({
killConfirm: "세션 {id} ({user})에서 실행 중인 쿼리를 취소하시겠습니까? 세션 연결은 유지됩니다.",
killSuccess: "세션 {id}에서 실행 중인 쿼리를 취소했습니다",
killFailed: "쿼리 취소에 실패했습니다: {message}",
terminate: "세션 종료",
cannotTerminateSelf: "현재 세션은 종료할 수 없습니다",
terminateTitle: "세션 종료",
terminateConfirm: "세션 {id} ({user})을(를) 종료하시겠습니까? 연결이 즉시 끊기고 커밋되지 않은 트랜잭션은 롤백됩니다.",
terminateSuccess: "세션 {id}을(를) 종료했습니다",
terminateFailed: "세션 종료에 실패했습니다: {message}",
previewTitle: "구문",
copy: "복사",
copied: "복사했습니다",
+6
View File
@@ -9539,6 +9539,12 @@ export default withEnglishFallback({
killConfirm: "Cancelar a consulta em execução da sessão {id} ({user})? A sessão permanecerá conectada.",
killSuccess: "Consulta em execução da sessão {id} cancelada.",
killFailed: "Falha ao cancelar a consulta: {message}",
terminate: "Encerrar sessão",
cannotTerminateSelf: "Não é possível encerrar a sessão atual",
terminateTitle: "Encerrar sessão",
terminateConfirm: "Encerrar a sessão {id} ({user})? A conexão é fechada imediatamente e qualquer transação não confirmada é revertida.",
terminateSuccess: "Sessão {id} encerrada",
terminateFailed: "Falha ao encerrar a sessão: {message}",
colPid: "PID",
colClient: "Cliente",
colApp: "Aplicativo",
+6
View File
@@ -3822,6 +3822,12 @@ export default withEnglishFallback({
killConfirm: "{id} ({user}) oturumundaki çalışan sorgu iptal edilsin mi? Oturum bağlı kalmaya devam eder.",
killSuccess: "{id} oturumundaki çalışan sorgu iptal edildi",
killFailed: "Sorgu iptal edilemedi: {message}",
terminate: "Oturumu sonlandır",
cannotTerminateSelf: "Kendi oturumunuzu sonlandıramazsınız",
terminateTitle: "Oturumu sonlandır",
terminateConfirm: "{id} ({user}) oturumu sonlandırılsın mı? Bağlantı hemen kapatılır ve işlenmemiş işlemler geri alınır.",
terminateSuccess: "{id} oturumu sonlandırıldı",
terminateFailed: "Oturum sonlandırılamadı: {message}",
previewTitle: "İfade",
copy: "Kopyala",
copied: "Kopyalandı",
+6
View File
@@ -3958,6 +3958,12 @@ export default withEnglishFallback({
killConfirm: "确定取消会话 {id}({user})中正在运行的查询?该会话将保持连接。",
killSuccess: "已取消会话 {id} 中正在运行的查询",
killFailed: "取消查询失败:{message}",
terminate: "终止会话",
cannotTerminateSelf: "无法终止当前会话",
terminateTitle: "终止会话",
terminateConfirm: "确定终止会话 {id}({user})?该连接会立即断开,未提交的事务将回滚。",
terminateSuccess: "已终止会话 {id}",
terminateFailed: "终止会话失败:{message}",
previewTitle: "语句",
copy: "复制",
copied: "已复制",
+6
View File
@@ -9537,6 +9537,12 @@ export default withEnglishFallback({
killConfirm: "確定取消會話 {id}({user})中執行中的查詢?該會話將保持連線。",
killSuccess: "已取消會話 {id} 中執行中的查詢",
killFailed: "取消查詢失敗:{message}",
terminate: "終止會話",
cannotTerminateSelf: "無法終止目前會話",
terminateTitle: "終止會話",
terminateConfirm: "確定終止會話 {id}({user})?該連線會立即中斷,未提交的交易將回滾。",
terminateSuccess: "已終止會話 {id}",
terminateFailed: "終止會話失敗:{message}",
colPid: "PID",
colClient: "用戶端",
colApp: "應用程式",
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { QueryResult } from "@/types/database";
import { buildCancelQuerySql, clampInterval, createProcessListLoadCoordinator, mapProcessRows, processListExecutionError, processListSessionCount, supportsProcessList } from "@/lib/database/mysqlProcessList";
import { buildCancelQuerySql, buildTerminateSessionSql, clampInterval, createProcessListLoadCoordinator, mapProcessRows, processListExecutionError, processListSessionCount, supportsProcessList } from "@/lib/database/mysqlProcessList";
function result(columns: string[], rows: (string | number | boolean | null)[][]): QueryResult {
return { columns, rows, affected_rows: 0, execution_time_ms: 0 };
@@ -48,6 +48,19 @@ describe("buildCancelQuerySql", () => {
});
});
describe("buildTerminateSessionSql", () => {
it("builds KILL for a valid id so an idle session is really disconnected", () => {
expect(buildTerminateSessionSql(8213)).toBe("KILL 8213");
});
it("rejects non-integer or nonpositive ids", () => {
expect(() => buildTerminateSessionSql(1.5)).toThrow();
expect(() => buildTerminateSessionSql(0)).toThrow();
expect(() => buildTerminateSessionSql(-1)).toThrow();
expect(() => buildTerminateSessionSql(Number.NaN)).toThrow();
});
});
describe("clampInterval", () => {
it("clamps below the minimum to 1 second", () => {
expect(clampInterval(0)).toBe(1);
@@ -3,10 +3,14 @@ import type { QueryResult } from "@/types/database";
import {
buildKingbaseCancelQuerySql,
buildKingbasePgCancelQuerySql,
buildKingbasePgTerminateSessionSql,
buildKingbaseTerminateSessionSql,
buildPgCancelQuerySql,
buildPgTerminateSessionSql,
isKingbaseOwnSessionCatalogCompatibilityError,
isKingbaseProcessListCatalogCompatibilityError,
isKingbaseCancelCatalogCompatibilityError,
isKingbaseTerminateCatalogCompatibilityError,
isPgProcessListCompatibilityError,
KINGBASE_OWN_SESSION_SQL,
KINGBASE_PG_OWN_SESSION_SQL,
@@ -18,6 +22,7 @@ import {
kingbaseCancelQueryResultError,
kingbasePgCancelQueryResultError,
pgCancelQueryResultError,
pgTerminateSessionResultError,
PG_PROCESS_LIST_LEGACY_SQL,
PG_PROCESS_LIST_SQL,
} from "@/lib/database/postgresProcessList";
@@ -120,6 +125,32 @@ describe("PostgreSQL-family process SQL", () => {
});
});
describe("session termination statements", () => {
it("builds pg_terminate_backend so an idle backend is really disconnected", () => {
expect(buildPgTerminateSessionSql(4211)).toBe("SELECT pg_terminate_backend(4211)");
expect(buildKingbaseTerminateSessionSql(4211)).toBe("SELECT sys_terminate_backend(4211)");
expect(buildKingbasePgTerminateSessionSql(4211)).toBe("SELECT pg_terminate_backend(4211)");
});
it("rejects non-integer or nonpositive pids", () => {
expect(() => buildPgTerminateSessionSql(1.5)).toThrow();
expect(() => buildPgTerminateSessionSql(0)).toThrow();
expect(() => buildPgTerminateSessionSql(Number.NaN)).toThrow();
});
it("requires the server to confirm that a backend was terminated", () => {
expect(pgTerminateSessionResultError([result(["pg_terminate_backend"], [[true]])])).toBeNull();
expect(pgTerminateSessionResultError([result(["pg_terminate_backend"], [["t"]])])).toBeNull();
expect(pgTerminateSessionResultError([result(["pg_terminate_backend"], [[false]])])).toContain("did not terminate");
expect(pgTerminateSessionResultError([result(["pg_terminate_backend"], [])])).toContain("did not terminate");
});
it("detects the Kingbase fallback trigger for a missing sys_terminate_backend", () => {
expect(isKingbaseTerminateCatalogCompatibilityError(new Error("function sys_terminate_backend(integer) does not exist"))).toBe(true);
expect(isKingbaseTerminateCatalogCompatibilityError(new Error("permission denied"))).toBe(false);
});
});
describe("Postgres compatibility", () => {
it("provides a pre-9.6 query and only falls back for missing wait-event columns", () => {
expect(PG_PROCESS_LIST_SQL).toContain("wait_event_type");
@@ -145,7 +176,10 @@ describe("resolveProcessListDriver", () => {
const mysql = resolveProcessListDriver("mysql");
const postgres = resolveProcessListDriver("postgres");
expect(mysql?.buildCancelQuerySql(7)).toBe("KILL QUERY 7");
expect(mysql?.buildTerminateSessionSql?.(7)).toBe("KILL 7");
expect(postgres?.buildCancelQuerySql(7)).toBe("SELECT pg_cancel_backend(7)");
expect(postgres?.buildTerminateSessionSql?.(7)).toBe("SELECT pg_terminate_backend(7)");
expect(postgres?.terminateSessionResultError?.([result(["pg_terminate_backend"], [[false]])])).toContain("did not terminate");
expect(postgres?.fallbackListSql).toBe(PG_PROCESS_LIST_LEGACY_SQL);
expect(postgres?.shouldUseFallbackListSql?.(new Error('column "wait_event" does not exist'))).toBe(true);
expect(postgres?.cancelQueryResultError?.([result(["pg_cancel_backend"], [[false]])])).toContain("did not cancel");
@@ -177,6 +211,8 @@ describe("connectionSupportsProcessList", () => {
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(resolveProcessListDriver("kingbase")?.buildTerminateSessionSql?.(7)).toBe("SELECT sys_terminate_backend(7)");
expect(resolveProcessListDriver("opengauss")?.buildTerminateSessionSql?.(7)).toBe("SELECT pg_terminate_backend(7)");
expect(connectionSupportsProcessList(conn({ db_type: "sqlite" }))).toBe(false);
expect(connectionSupportsProcessList(undefined)).toBe(false);
});
@@ -137,6 +137,21 @@ export function buildCancelQuerySql(id: number): string {
return `KILL QUERY ${id}`;
}
/**
* Build the `KILL <id>` statement that disconnects the session itself.
*
* `KILL QUERY` only interrupts the statement running at that instant, so an idle
* (`Sleep`) session survives it and keeps showing up in the process list. When the
* user asks to close a session, only this statement actually removes it (and rolls
* back the session's uncommitted transaction, the same as the server-side `KILL`).
*/
export function buildTerminateSessionSql(id: number): string {
if (!Number.isInteger(id) || id <= 0) {
throw new Error(`Invalid session id: ${id}`);
}
return `KILL ${id}`;
}
/** Clamp a user-entered refresh interval to a safe integer range of seconds. */
export function clampInterval(seconds: number): number {
if (!Number.isFinite(seconds)) return DEFAULT_REFRESH_SECONDS;
@@ -174,6 +174,27 @@ export function buildKingbasePgCancelQuerySql(pid: number): string {
return `SELECT pg_cancel_backend(${pid})`;
}
/**
* Build the statement that disconnects the backend instead of only interrupting the
* statement it is running right now. `pg_cancel_backend` cancels the current query and
* leaves an idle backend alive, so the process list keeps listing it; terminating the
* backend is what actually removes the session (rolling back its open transaction).
*/
export function buildPgTerminateSessionSql(pid: number): string {
validateBackendPid(pid);
return `SELECT pg_terminate_backend(${pid})`;
}
export function buildKingbaseTerminateSessionSql(pid: number): string {
validateBackendPid(pid);
return `SELECT sys_terminate_backend(${pid})`;
}
export function buildKingbasePgTerminateSessionSql(pid: number): string {
validateBackendPid(pid);
return `SELECT pg_terminate_backend(${pid})`;
}
/** Return an error when PostgreSQL reports that no running query was canceled. */
export function pgCancelQueryResultError(results: QueryResult[]): string | null {
return backendCancelQueryResultError(results, "pg_cancel_backend");
@@ -187,11 +208,24 @@ export function kingbasePgCancelQueryResultError(results: QueryResult[]): string
return backendCancelQueryResultError(results, "pg_cancel_backend");
}
function backendCancelQueryResultError(results: QueryResult[], functionName: string): string | null {
/** Return an error when the server reports that no backend was terminated. */
export function pgTerminateSessionResultError(results: QueryResult[]): string | null {
return backendCancelQueryResultError(results, "pg_terminate_backend", "did not terminate the session");
}
export function kingbaseTerminateSessionResultError(results: QueryResult[]): string | null {
return backendCancelQueryResultError(results, "sys_terminate_backend", "did not terminate the session");
}
export function kingbasePgTerminateSessionResultError(results: QueryResult[]): string | null {
return backendCancelQueryResultError(results, "pg_terminate_backend", "did not terminate the session");
}
function backendCancelQueryResultError(results: QueryResult[], functionName: string, failure = "did not cancel a running query"): 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 `${functionName} did not cancel a running query`;
return `${functionName} ${failure}`;
}
/** Detect the undefined-column failure produced by pre-9.6 pg_stat_activity. */
@@ -213,3 +247,7 @@ export function isKingbaseOwnSessionCatalogCompatibilityError(error: unknown): b
export function isKingbaseCancelCatalogCompatibilityError(error: unknown): boolean {
return isMissingKingbaseSysFunction(error, ["sys_cancel_backend"]);
}
export function isKingbaseTerminateCatalogCompatibilityError(error: unknown): boolean {
return isMissingKingbaseSysFunction(error, ["sys_terminate_backend"]);
}
@@ -1,16 +1,22 @@
import type { ConnectionConfig, DatabaseType, QueryResult } from "@/types/database";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
import { buildCancelQuerySql as buildMysqlCancelQuerySql, mapProcessRows as mapMysqlProcessRows, PROCESS_LIST_SQL as MYSQL_PROCESS_LIST_SQL, supportsProcessList as supportsMysqlProcessList } from "./mysqlProcessList";
import { buildCancelQuerySql as buildMysqlCancelQuerySql, buildTerminateSessionSql as buildMysqlTerminateSessionSql, mapProcessRows as mapMysqlProcessRows, PROCESS_LIST_SQL as MYSQL_PROCESS_LIST_SQL, supportsProcessList as supportsMysqlProcessList } from "./mysqlProcessList";
import {
buildKingbaseCancelQuerySql,
buildKingbasePgCancelQuerySql,
buildKingbasePgTerminateSessionSql,
buildKingbaseTerminateSessionSql,
buildPgCancelQuerySql,
buildPgTerminateSessionSql,
isKingbaseOwnSessionCatalogCompatibilityError,
isKingbaseProcessListCatalogCompatibilityError,
isKingbaseCancelCatalogCompatibilityError,
isKingbaseTerminateCatalogCompatibilityError,
isPgProcessListCompatibilityError,
kingbaseCancelQueryResultError,
kingbasePgCancelQueryResultError,
kingbasePgTerminateSessionResultError,
kingbaseTerminateSessionResultError,
KINGBASE_OWN_SESSION_SQL,
KINGBASE_PG_OWN_SESSION_SQL,
KINGBASE_PG_PROCESS_LIST_SQL,
@@ -19,6 +25,7 @@ import {
OPENGAUSS_OWN_SESSION_SQL,
OPENGAUSS_PROCESS_LIST_SQL,
pgCancelQueryResultError,
pgTerminateSessionResultError,
PG_OWN_SESSION_SQL,
PG_PROCESS_LIST_LEGACY_SQL,
PG_PROCESS_LIST_SQL,
@@ -79,6 +86,19 @@ export interface ProcessListDriver {
cancelQueryResultError?(results: QueryResult[]): string | null;
/** Validate the success value returned by the compatibility cancellation statement. */
fallbackCancelQueryResultError?(results: QueryResult[]): string | null;
/**
* Build the validated statement that disconnects the session. Engines that omit it
* only offer query cancellation, because they cannot close a session over the wire.
*/
buildTerminateSessionSql?(id: number): string;
/** Build the compatibility statement used when the primary terminate function is unavailable. */
buildFallbackTerminateSessionSql?(id: number): string;
/** Restrict terminate fallback attempts to known compatibility failures. */
shouldUseFallbackTerminateSessionSql?(error: unknown): boolean;
/** Validate any engine-specific success value returned by the terminate statement. */
terminateSessionResultError?(results: QueryResult[]): string | null;
/** Validate the success value returned by the compatibility terminate statement. */
fallbackTerminateSessionResultError?(results: QueryResult[]): string | null;
}
const MYSQL_COLUMNS: ProcessColumn[] = [
@@ -114,6 +134,7 @@ const MYSQL_DRIVER: ProcessListDriver = {
// Typed structs carry no index signature; they are plain string-keyed objects at runtime.
mapRows: (result) => mapMysqlProcessRows(result) as unknown as ProcessRow[],
buildCancelQuerySql: buildMysqlCancelQuerySql,
buildTerminateSessionSql: buildMysqlTerminateSessionSql,
};
const POSTGRES_DRIVER: ProcessListDriver = {
@@ -127,6 +148,8 @@ const POSTGRES_DRIVER: ProcessListDriver = {
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
buildCancelQuerySql: buildPgCancelQuerySql,
cancelQueryResultError: pgCancelQueryResultError,
buildTerminateSessionSql: buildPgTerminateSessionSql,
terminateSessionResultError: pgTerminateSessionResultError,
};
const OPENGAUSS_DRIVER: ProcessListDriver = {
@@ -138,6 +161,8 @@ const OPENGAUSS_DRIVER: ProcessListDriver = {
mapRows: (result) => mapPgProcessRows(result) as unknown as ProcessRow[],
buildCancelQuerySql: buildPgCancelQuerySql,
cancelQueryResultError: pgCancelQueryResultError,
buildTerminateSessionSql: buildPgTerminateSessionSql,
terminateSessionResultError: pgTerminateSessionResultError,
};
const KINGBASE_DRIVER: ProcessListDriver = {
@@ -156,6 +181,11 @@ const KINGBASE_DRIVER: ProcessListDriver = {
shouldUseFallbackCancelQuerySql: isKingbaseCancelCatalogCompatibilityError,
cancelQueryResultError: kingbaseCancelQueryResultError,
fallbackCancelQueryResultError: kingbasePgCancelQueryResultError,
buildTerminateSessionSql: buildKingbaseTerminateSessionSql,
buildFallbackTerminateSessionSql: buildKingbasePgTerminateSessionSql,
shouldUseFallbackTerminateSessionSql: isKingbaseTerminateCatalogCompatibilityError,
terminateSessionResultError: kingbaseTerminateSessionResultError,
fallbackTerminateSessionResultError: kingbasePgTerminateSessionResultError,
};
/** Resolve the process-list driver for a connection, or null if unsupported. */
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB