feat(query): add manual transaction controls

This commit is contained in:
Abeautifulsnow authored and GitHub committed 2026-07-02 01:30:31 +08:00
1 parent f871e00330
commit d55a708d6a
27 files changed
+927 -40

No files matched your search

+11
View File
@@ -1622,8 +1622,19 @@ onUnmounted(() => {
:block-dangerous-redis-commands="blockDangerousRedisCommands"
:sql-keyword-case="settingsStore.editorSettings.sqlFormatter.keywordCase"
:database-required-signal="databaseRequiredTabId === activeTab.id ? databaseRequiredSignal : 0"
:auto-commit="activeTab.autoCommit ?? true"
:txn-session-id="activeTab?.txnSessionId"
:txn-auto-rolled-back="activeTab?.txnAutoRolledBack"
@update:explain-mode="(m: 'explain' | 'autotrace') => (explainMode = m)"
@update:block-dangerous-redis-commands="(v: boolean) => (blockDangerousRedisCommands = v)"
@update:auto-commit="
(v: boolean) => {
if (activeTab) queryStore.setAutoCommit(activeTab.id, v);
}
"
@commit="activeTab && queryStore.commitTransaction(activeTab.id)"
@rollback="activeTab && queryStore.rollbackTransaction(activeTab.id)"
@dismiss-txn-rolled-back="activeTab && (activeTab.txnAutoRolledBack = false)"
@execute="requestActiveEditorExecute()"
@cancel="cancelActiveExecution()"
@explain="tryExplain()"
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed, ref, watch, watchEffect } from "vue";
import { useI18n } from "vue-i18n";
import { Play, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, Layers, X, Shield, Upload } from "@lucide/vue";
import { Play, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, Layers, X, Shield, Upload, RotateCcw, AlertTriangle } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { SearchableSelect } from "@/components/ui/searchable-select";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
@@ -13,7 +13,7 @@ import { useSchemaOptions } from "@/composables/useSchemaOptions";
import { connectionIconType } from "@/lib/connectionPresentation";
import { formatDatabaseLabel, isDefaultDatabase } from "@/lib/defaultDatabase";
import { connectionDisplayName } from "@/lib/tabPresentation";
import { isSingleDatabase } from "@/lib/databaseCapabilities";
import { isSingleDatabase, supportsTransaction as supportsTransactionFeature } from "@/lib/databaseCapabilities";
import { hexToRgba } from "@/lib/color";
import type { QueryTab, ConnectionConfig } from "@/types/database";
@@ -25,6 +25,9 @@ const props = defineProps<{
blockDangerousRedisCommands?: boolean;
sqlKeywordCase: "preserve" | "upper" | "lower";
databaseRequiredSignal?: number;
autoCommit?: boolean;
txnSessionId?: string;
txnAutoRolledBack?: boolean;
}>();
const emit = defineEmits<{
@@ -43,6 +46,10 @@ const emit = defineEmits<{
setDefaultDatabase: [];
clearDefaultDatabase: [];
"update:blockDangerousRedisCommands": [value: boolean];
"update:autoCommit": [value: boolean];
commit: [];
rollback: [];
dismissTxnRolledBack: [];
}>();
const { t } = useI18n();
@@ -64,12 +71,22 @@ const supportsExplain = computed(() => {
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "chromadb" && dbType !== "etcd" && dbType !== "zookeeper" && dbType !== "mq" && dbType !== "nacos";
});
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
const supportsTransaction = computed(() => supportsTransactionFeature(props.activeConnection?.db_type));
const hasDefaultDatabaseOption = computed(() => activeDatabaseOptions.value.includes(""));
const schemaDatabaseKey = computed(() => props.activeTab.database || (isSingleDb.value ? "_" : ""));
const saveTooltip = computed(() => (props.activeTab.objectSource ? t("objects.saveSource") : t("toolbar.saveSql")));
const canSaveSql = computed(() => !!props.activeTab.externalSqlPath || !!props.activeTab.sql.trim());
const keywordCaseIsLower = computed(() => props.sqlKeywordCase === "lower");
const keywordCaseToggleTooltip = computed(() => (keywordCaseIsLower.value ? t("toolbar.keywordCaseUpper") : t("toolbar.keywordCaseLower")));
const transactionTooltip = computed(() => {
const isAgent = (props.activeConnection?.db_type as string) === "agent";
const isManual = props.autoCommit === false;
if (isAgent && isManual) return t("toolbar.manualTransactionAgent");
if (isAgent) return t("toolbar.autoCommitAgent");
return isManual ? t("toolbar.manualTransaction") : t("toolbar.autoCommit");
});
const isTransactionActive = computed(() => !!props.txnSessionId);
const showSchemaSelector = computed(() => {
const connection = props.activeConnection;
@@ -175,6 +192,41 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
>
<span class="font-bold" style="font-size: 9px">A</span>
</Button>
<!-- Transaction toggle -->
<Tooltip v-if="supportsTransaction">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-6 w-6"
:class="isTransactionActive || autoCommit === false ? 'text-orange-600 bg-orange-100 dark:text-orange-300 dark:bg-orange-900/30' : 'text-muted-foreground/50'"
:disabled="activeTab.isExecuting || activeTab.isExplaining"
@click="emit('update:autoCommit', autoCommit === false)"
>
<span class="font-bold" style="font-size: 9px">Tx</span>
</Button>
</TooltipTrigger>
<TooltipContent>{{ transactionTooltip }}</TooltipContent>
</Tooltip>
<!-- Commit button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-green-600 hover:bg-green-500/10 hover:text-green-700 dark:text-green-300 dark:hover:text-green-200" :disabled="activeTab.isExecuting" @click="emit('commit')">
<Check class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.commit") }}</TooltipContent>
</Tooltip>
<!-- Rollback button (only when transaction is active) -->
<Tooltip v-if="isTransactionActive">
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-red-600 hover:bg-red-500/10 hover:text-red-700 dark:text-red-300 dark:hover:text-red-200" :disabled="activeTab.isExecuting" @click="emit('rollback')">
<RotateCcw class="h-3.5 w-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>{{ t("toolbar.rollback") }}</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger as-child>
<Button variant="ghost" size="icon" class="h-6 w-6 text-amber-600 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200" :disabled="activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()" @click="emit('formatSql')">
@@ -342,6 +394,13 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
<span class="truncate">{{ activeTab.tableMeta.columns.length }} {{ t("tree.columns") }}</span>
</div>
</div>
<div v-if="txnAutoRolledBack" class="flex items-center gap-2 px-3 py-1 text-xs bg-amber-500/10 text-amber-700 dark:text-amber-300 border-b border-amber-500/20">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<span>{{ t("toolbar.txnAutoRolledBack") }}</span>
<Button variant="ghost" size="icon" class="h-5 w-5 ml-auto" @click="emit('dismissTxnRolledBack')">
<X class="h-3 w-3" />
</Button>
</div>
</template>
<style scoped>
+7
View File
@@ -34,6 +34,13 @@ export default {
formatSqlFailed: "Failed to format SQL",
keywordCaseLower: "Use lower-case SQL keywords",
keywordCaseUpper: "Use upper-case SQL keywords",
autoCommit: "Auto Commit",
manualTransaction: "Manual Transaction",
autoCommitAgent: "Auto Commit (depends on Agent backend)",
manualTransactionAgent: "Manual Transaction (depends on Agent backend)",
commit: "Commit",
rollback: "Rollback",
txnAutoRolledBack: "Transaction auto-rolled back after 5 minutes of inactivity",
previewSql: "Preview SQL",
hidePreviewSql: "Hide SQL Preview",
saveSql: "Save to SQL Library",
+7
View File
@@ -36,6 +36,13 @@ export default withEnglishFallback({
formatSqlFailed: "Error al formatear el SQL",
keywordCaseLower: "Usar palabras clave SQL en minúsculas",
keywordCaseUpper: "Usar palabras clave SQL en mayúsculas",
autoCommit: "Auto Commit",
manualTransaction: "Transacción Manual",
autoCommitAgent: "Auto Commit (depende del backend del Agente)",
manualTransactionAgent: "Transacción Manual (depende del backend del Agente)",
commit: "Confirmar",
rollback: "Revertir",
txnAutoRolledBack: "Transacción revertida automáticamente tras 5 minutos de inactividad",
previewSql: "Vista previa de SQL",
hidePreviewSql: "Ocultar vista previa de SQL",
saveSql: "Guardar en biblioteca SQL",
+7
View File
@@ -35,6 +35,13 @@ export default withEnglishFallback({
formatSqlFailed: "Impossibile formattare SQL",
keywordCaseLower: "Usa parole chiave SQL minuscole",
keywordCaseUpper: "Usa parole chiave SQL maiuscole",
autoCommit: "Auto Commit",
manualTransaction: "Transazione Manuale",
autoCommitAgent: "Auto Commit (dipende dal backend dell'Agente)",
manualTransactionAgent: "Transazione Manuale (dipende dal backend dell'Agente)",
commit: "Commit",
rollback: "Rollback",
txnAutoRolledBack: "Transazione annullata automaticamente dopo 5 minuti di inattività",
previewSql: "Anteprima SQL",
hidePreviewSql: "Nascondi anteprima SQL",
saveSql: "Salva nella Libreria SQL",
+7
View File
@@ -36,6 +36,13 @@ export default withEnglishFallback({
formatSqlFailed: "SQLのフォーマットに失敗しました",
keywordCaseLower: "SQLキーワードを小文字にする",
keywordCaseUpper: "SQLキーワードを大文字にする",
autoCommit: "自動コミット",
manualTransaction: "手動トランザクション",
autoCommitAgent: "自動コミット(Agentバックエンドに依存)",
manualTransactionAgent: "手動トランザクション(Agentバックエンドに依存)",
commit: "コミット",
rollback: "ロールバック",
txnAutoRolledBack: "5分間操作がなかったため、トランザクションは自動的にロールバックされました",
previewSql: "SQLをプレビュー",
hidePreviewSql: "SQLプレビューを非表示",
saveSql: "SQLライブラリに保存",
+7
View File
@@ -36,6 +36,13 @@ export default withEnglishFallback({
formatSqlFailed: "Falha ao formatar SQL",
keywordCaseLower: "Usar palavras-chave SQL em minúsculas",
keywordCaseUpper: "Usar palavras-chave SQL em maiúsculas",
autoCommit: "Auto Commit",
manualTransaction: "Transação Manual",
autoCommitAgent: "Auto Commit (depende do backend do Agente)",
manualTransactionAgent: "Transação Manual (depende do backend do Agente)",
commit: "Confirmar",
rollback: "Reverter",
txnAutoRolledBack: "Transação revertida automaticamente após 5 minutos de inatividade",
previewSql: "Visualizar SQL",
hidePreviewSql: "Ocultar Visualização SQL",
saveSql: "Salvar na Biblioteca SQL",
+7
View File
@@ -36,6 +36,13 @@ export default withEnglishFallback({
formatSqlFailed: "SQL 格式化失败",
keywordCaseLower: "使用小写 SQL 关键字",
keywordCaseUpper: "使用大写 SQL 关键字",
autoCommit: "自动提交",
manualTransaction: "手动事务",
autoCommitAgent: "自动提交(取决于 Agent 后端数据库)",
manualTransactionAgent: "手动事务(取决于 Agent 后端数据库)",
commit: "提交",
rollback: "回滚",
txnAutoRolledBack: "事务已因 5 分钟无操作而自动回滚",
previewSql: "预览 SQL",
hidePreviewSql: "隐藏 SQL 预览",
saveSql: "保存到 SQL 库",
+7
View File
@@ -36,6 +36,13 @@ export default withEnglishFallback({
formatSqlFailed: "SQL 格式化失敗",
keywordCaseLower: "使用小寫 SQL 關鍵字",
keywordCaseUpper: "使用大寫 SQL 關鍵字",
autoCommit: "自動提交",
manualTransaction: "手動事務",
autoCommitAgent: "自動提交(取決於 Agent 後端資料庫)",
manualTransactionAgent: "手動事務(取決於 Agent 後端資料庫)",
commit: "提交",
rollback: "回溯",
txnAutoRolledBack: "事務已因 5 分鐘無操作而自動回溯",
previewSql: "預覽 SQL",
hidePreviewSql: "隱藏 SQL 預覽",
saveSql: "儲存到 SQL 庫",
@@ -0,0 +1,28 @@
import { describe, expect, it } from "vitest";
import { supportsTransaction } from "@/lib/databaseFeatureSupport";
describe("supportsTransaction", () => {
it("returns true for supported database types", () => {
expect(supportsTransaction("postgres")).toBe(true);
expect(supportsTransaction("mysql")).toBe(true);
});
it("returns false for unsupported database types", () => {
expect(supportsTransaction("redis")).toBe(false);
expect(supportsTransaction("mongodb")).toBe(false);
expect(supportsTransaction("duckdb")).toBe(false);
expect(supportsTransaction("qdrant")).toBe(false);
expect(supportsTransaction("turso")).toBe(false);
expect(supportsTransaction("sqlite")).toBe(false);
expect(supportsTransaction("clickhouse")).toBe(false);
expect(supportsTransaction("sqlserver")).toBe(false);
expect(supportsTransaction("oracle")).toBe(false);
expect(supportsTransaction("dameng")).toBe(false);
expect(supportsTransaction("rqlite")).toBe(false);
expect(supportsTransaction("agent")).toBe(false);
});
it("returns false for undefined or empty input", () => {
expect(supportsTransaction(undefined)).toBe(false);
});
});
+4
View File
@@ -142,6 +142,10 @@ export const executeMulti = forward("executeMulti");
export const executeBatch = forward("executeBatch");
export const executeScript = forward("executeScript");
export const executeInTransaction = forward("executeInTransaction");
export const beginManualTransaction = forward("beginManualTransaction");
export const executeInManualTransaction = forward("executeInManualTransaction");
export const commitManualTransaction = forward("commitManualTransaction");
export const rollbackManualTransaction = forward("rollbackManualTransaction");
export const cancelQuery = forward("cancelQuery");
export const closeQuerySession = forward("closeQuerySession");
export const closeClientConnectionSession = forward("closeClientConnectionSession");
@@ -88,3 +88,13 @@ export function supportsTableTruncate(dbType?: DatabaseType): boolean {
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
}
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql"];
/**
* Returns true if the given database type supports explicit transaction control
* (i.e. toggling between auto-commit and manual transaction mode via BEGIN/COMMIT).
*/
export function supportsTransaction(dbType?: string): boolean {
return !!dbType && TRANSACTION_SUPPORTED_TYPES.includes(dbType);
}
+16
View File
@@ -657,6 +657,22 @@ export async function executeInTransaction(connectionId: string, database: strin
return post("/api/query/execute-in-transaction", { connectionId, database, statements, schema });
}
export async function beginManualTransaction(_connectionId: string, _database: string, _schema?: string): Promise<string> {
throw new Error("Manual transaction management is only available in the desktop app.");
}
export async function executeInManualTransaction(_txnSessionId: string, _sql: string, _database: string, _schema?: string, _maxRows?: number): Promise<QueryResult[]> {
throw new Error("Manual transaction management is only available in the desktop app.");
}
export async function commitManualTransaction(_txnSessionId: string): Promise<QueryResult> {
throw new Error("Manual transaction management is only available in the desktop app.");
}
export async function rollbackManualTransaction(_txnSessionId: string): Promise<QueryResult> {
throw new Error("Manual transaction management is only available in the desktop app.");
}
export async function cancelQuery(executionId: string): Promise<boolean> {
const result = await post<boolean | { cancelled?: boolean }>("/api/query/cancel", { executionId });
return typeof result === "boolean" ? result : result.cancelled === true;
+17
View File
@@ -615,6 +615,7 @@ export async function executeMulti(
resultSessionId?: string;
clientSessionId?: string;
timeoutSecs?: number;
useTransaction?: boolean;
},
): Promise<QueryResult[]> {
return invoke("execute_multi", { connectionId, database, sql, schema, executionId, ...options });
@@ -648,6 +649,22 @@ export async function executeInTransaction(connectionId: string, database: strin
return invoke("execute_in_transaction", { connectionId, database, statements, schema });
}
export async function beginManualTransaction(connectionId: string, database: string, schema?: string): Promise<string> {
return invoke("begin_manual_transaction", { connectionId, database, schema });
}
export async function executeInManualTransaction(txnSessionId: string, sql: string, database: string, schema?: string, maxRows?: number): Promise<QueryResult[]> {
return invoke("execute_in_manual_transaction", { txnSessionId, sql, database, schema, maxRows });
}
export async function commitManualTransaction(txnSessionId: string): Promise<QueryResult> {
return invoke("commit_manual_transaction", { txnSessionId });
}
export async function rollbackManualTransaction(txnSessionId: string): Promise<QueryResult> {
return invoke("rollback_manual_transaction", { txnSessionId });
}
export async function analyzeSqlReferences(sql: string, dialect?: string): Promise<SqlReferenceAnalysis> {
return invoke("analyze_sql_references", { sql, dialect });
}
@@ -1317,6 +1317,7 @@ export const useConnectionStore = defineStore("connection", () => {
queryStore.releaseConnectionTabs(connectionId);
break;
case "keep-tabs-keep-results":
queryStore.rollbackConnectionTransactions(connectionId);
break;
}
connectedIds.value.delete(connectionId);
@@ -1349,6 +1350,7 @@ export const useConnectionStore = defineStore("connection", () => {
queryStore.releaseDatabaseTabs(connectionId, database);
break;
case "keep-tabs-keep-results":
queryStore.rollbackDatabaseTransactions(connectionId, database);
break;
}
const node = findDatabaseTreeNode(treeNodes.value, connectionId, database);
+110 -23
View File
@@ -859,6 +859,7 @@ export const useQueryStore = defineStore("query", () => {
const idx = tabs.value.findIndex((t) => t.id === id);
if (idx < 0) return;
clearDataGridPendingSnapshotsForTab(id);
if (tabs.value[idx].txnSessionId) void rollbackTransaction(id);
if (tabs.value[idx].isExecuting) void cancelTabExecution(id);
if (tabs.value[idx].isExplaining) void cancelTabExplain(id);
void closeResultSession(tabs.value[idx]);
@@ -987,6 +988,7 @@ export const useQueryStore = defineStore("query", () => {
.filter((tab) => closingIds.has(tab.id))
.forEach((tab) => {
clearDataGridPendingSnapshotsForTab(tab.id);
if (tab.txnSessionId) void rollbackTransaction(tab.id);
if (tab.isExecuting) void cancelTabExecution(tab.id);
if (tab.isExplaining) void cancelTabExplain(tab.id);
void closeResultSession(tab);
@@ -1015,6 +1017,7 @@ export const useQueryStore = defineStore("query", () => {
tabs.value
.filter((tab) => predicate(tab))
.forEach((tab) => {
rollbackTabTransaction(tab, { resetAutoCommit: true });
if (tab.isExecuting) void cancelTabExecution(tab.id);
if (tab.isExplaining) void cancelTabExplain(tab.id);
void closeResultSession(tab);
@@ -1035,6 +1038,18 @@ export const useQueryStore = defineStore("query", () => {
return tabs.value.some((tab) => tab.connectionId === connectionId && tab.database === database);
}
function rollbackTabsWhere(predicate: (tab: QueryTab) => boolean, options?: { resetAutoCommit?: boolean }) {
tabs.value.filter((tab) => predicate(tab)).forEach((tab) => rollbackTabTransaction(tab, options));
}
function rollbackConnectionTransactions(connectionId: string) {
rollbackTabsWhere((tab) => tab.connectionId === connectionId, { resetAutoCommit: true });
}
function rollbackDatabaseTransactions(connectionId: string, database: string) {
rollbackTabsWhere((tab) => tab.connectionId === connectionId && tab.database === database, { resetAutoCommit: true });
}
function updateSql(id: string, sql: string) {
const tab = tabs.value.find((t) => t.id === id);
if (tab) {
@@ -1042,6 +1057,49 @@ export const useQueryStore = defineStore("query", () => {
}
}
function setAutoCommit(id: string, autoCommit: boolean) {
const tab = tabs.value.find((t) => t.id === id);
if (tab) {
const wasManual = tab.autoCommit === false;
tab.autoCommit = autoCommit;
if (autoCommit && wasManual) {
if (tab.txnSessionId) {
void rollbackTransaction(id);
} else {
tab.txnAutoRolledBack = false;
}
}
}
}
function rollbackTabTransaction(tab: QueryTab, options?: { resetAutoCommit?: boolean }) {
if (tab.txnSessionId) void rollbackTransaction(tab.id);
if (options?.resetAutoCommit) tab.autoCommit = true;
tab.txnAutoRolledBack = false;
}
async function commitTransaction(id: string) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab?.txnSessionId) return;
try {
await api.commitManualTransaction(tab.txnSessionId);
} finally {
tab.txnSessionId = undefined;
tab.txnAutoRolledBack = false;
}
}
async function rollbackTransaction(id: string) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab?.txnSessionId) return;
try {
await api.rollbackManualTransaction(tab.txnSessionId);
} finally {
tab.txnSessionId = undefined;
tab.txnAutoRolledBack = false;
}
}
function updateEditorViewport(id: string, viewport: { scrollTop: number; scrollLeft: number }) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab) return;
@@ -1154,6 +1212,7 @@ export const useQueryStore = defineStore("query", () => {
function updateDatabase(id: string, database: string) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || tab.database === database) return;
rollbackTabTransaction(tab);
void closeResultSession(tab);
void closeClientConnectionSession(tab);
tab.database = database;
@@ -1170,6 +1229,7 @@ export const useQueryStore = defineStore("query", () => {
function updateSchema(id: string, schema: string | undefined) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || tab.schema === schema) return;
rollbackTabTransaction(tab);
tab.schema = schema;
if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema };
}
@@ -1177,6 +1237,7 @@ export const useQueryStore = defineStore("query", () => {
function updateConnection(id: string, connectionId: string, database = "") {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || tab.connectionId === connectionId) return;
rollbackTabTransaction(tab, { resetAutoCommit: true });
void closeResultSession(tab);
void closeClientConnectionSession(tab);
tab.connectionId = connectionId;
@@ -1912,32 +1973,44 @@ export const useQueryStore = defineStore("query", () => {
return;
}
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
const clientSessionId = tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined;
const executionOptions = {
...(typeof pageLimit === "number"
? useAgentResultSession
? {
fetchSize: pageLimit,
pageSize: pageLimit,
resultSessionId: options?.pagination?.sessionId,
}
: { maxRows: pageLimit, fetchSize: pageLimit }
: {}),
...(clientSessionId ? { clientSessionId } : {}),
timeoutSecs: queryTimeoutSecs,
};
const executionSchema = connectionUsesSchemaExecutionContext(conn) ? tab.schema || tab.database : tab.mode === "data" || connectionUsesDatabaseObjectTreeMode(conn) ? undefined : tab.schema;
console.info("[DBX][executeTabSql:execute-multi:invoke]", {
traceId,
elapsed: elapsed(),
executionSchema,
optionKeys: Object.keys(executionOptions),
clientSession: Boolean(clientSessionId),
});
const executionPromise = api.executeMulti(tab.connectionId, tab.database, sqlToExecute, executionSchema, executionId, executionOptions);
const frontendTimeoutSecs = Math.max(queryTimeoutSecs * 2, 60);
const sourceLabelDatabase = tab.database || conn?.database;
let executionPromise: Promise<QueryResult[]>;
if (tab.autoCommit === false) {
if (!tab.txnSessionId) {
console.info("[DBX][executeTabSql:begin-manual-txn:start]", { traceId, elapsed: elapsed() });
tab.txnSessionId = await api.beginManualTransaction(tab.connectionId, tab.database, executionSchema);
console.info("[DBX][executeTabSql:begin-manual-txn:done]", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
}
console.info("[DBX][executeTabSql:execute-in-txn:invoke]", { traceId, txnSessionId: tab.txnSessionId, elapsed: elapsed() });
executionPromise = api.executeInManualTransaction(tab.txnSessionId, sqlToExecute, tab.database, executionSchema, pageLimit);
} else {
console.info("[DBX][executeTabSql:execute-multi:start]", { traceId, elapsed: elapsed() });
const clientSessionId = tab.mode === "query" || tab.mode === "data" ? tabClientSessionId(tab) : undefined;
const executionOptions = {
...(typeof pageLimit === "number"
? useAgentResultSession
? {
fetchSize: pageLimit,
pageSize: pageLimit,
resultSessionId: options?.pagination?.sessionId,
}
: { maxRows: pageLimit, fetchSize: pageLimit }
: {}),
...(clientSessionId ? { clientSessionId } : {}),
timeoutSecs: queryTimeoutSecs,
};
console.info("[DBX][executeTabSql:execute-multi:invoke]", {
traceId,
elapsed: elapsed(),
executionSchema,
optionKeys: Object.keys(executionOptions),
clientSession: Boolean(clientSessionId),
});
executionPromise = api.executeMulti(tab.connectionId, tab.database, sqlToExecute, executionSchema, executionId, executionOptions);
}
const results = annotateQueryResultSources(markQueryResultsRowsRaw(await withFrontendQueryTimeout(executionPromise, queryTimeoutSecs === 0 ? 0 : frontendTimeoutSecs, t("editor.queryTimeoutError", { seconds: frontendTimeoutSecs }))), queryBaseSql, sourceLabelDatabase, effectiveDbType);
console.info("[DBX][executeTabSql:execute-multi:done]", {
traceId,
@@ -2016,6 +2089,15 @@ export const useQueryStore = defineStore("query", () => {
console.error("[DBX][executeTabSql:error]", { traceId, elapsed: elapsed(), error: e });
// Sync connection state if the error indicates a lost connection
useConnectionStore().recordConnectionLostError(tab.connectionId, e);
// Handle manual transaction auto-rollback (e.g. deadlock detected by server,
// statement error inside a manual transaction, or idle timeout).
if (tab.autoCommit === false) {
const errMsg: string = e?.message ?? String(e);
if (/rolled.?back/i.test(errMsg) || errMsg.includes("已自动回滚")) {
tab.txnSessionId = undefined;
tab.txnAutoRolledBack = true;
}
}
const current = tabs.value.find((t) => t.id === id);
if (current?.executionId === executionId) {
current.result = toErrorResult(e);
@@ -2581,9 +2663,14 @@ export const useQueryStore = defineStore("query", () => {
releaseConnectionTabs,
releaseDatabaseTabs,
isDatabaseOpen,
rollbackConnectionTransactions,
rollbackDatabaseTransactions,
updateSql,
updateEditorViewport,
updateEditorSelection,
setAutoCommit,
commitTransaction,
rollbackTransaction,
renameTab,
openObjectBrowser,
openUserAdmin,
+7
View File
@@ -687,6 +687,13 @@ export interface QueryTab {
resultEvicted?: boolean;
whereInput?: string;
previewSql?: string;
/** Whether to use auto-commit mode (default true). When false, multiple statements are
* wrapped in a single transaction. */
autoCommit?: boolean;
/** Session ID for an active manual transaction, set after beginManualTransaction */
txnSessionId?: string;
/** Set to true when a manual transaction was auto-rolled back due to inactivity */
txnAutoRolledBack?: boolean;
}
export interface SavedSqlFolder {