feat(query): add manual transaction controls
This commit is contained in:
1 parent
f871e00330
commit
d55a708d6a
27 files changed
+927
-40
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ライブラリに保存",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 库",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user