feat(oracle): support manual query transactions
This commit is contained in:
22 files changed
+1011
-99
No files matched your search
@@ -150,9 +150,12 @@ function toggleSqlSemanticDiagnostics() {
|
||||
sqlSemanticDiagnosticsMode: sqlSemanticDiagnosticsEnabled.value ? "disabled" : "enabled",
|
||||
});
|
||||
}
|
||||
const isTransactionActive = computed(() => !!props.txnSessionId);
|
||||
const isManualTransactionMode = computed(() => props.autoCommit === false || isTransactionActive.value);
|
||||
const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));
|
||||
const transactionTooltip = computed(() => {
|
||||
const isAgent = (props.activeConnection?.db_type as string) === "agent";
|
||||
const isManual = props.autoCommit === false;
|
||||
const isManual = isManualTransactionMode.value;
|
||||
if (isAgent && isManual) return t("toolbar.manualTransactionAgent");
|
||||
if (isAgent) return t("toolbar.autoCommitAgent");
|
||||
return isManual ? t("toolbar.manualTransaction") : t("toolbar.autoCommit");
|
||||
@@ -162,7 +165,6 @@ const executeButtonClass = computed(() => {
|
||||
return activeProductionContext.value.active ? "bg-red-500/10 text-red-700 hover:bg-red-500/20 hover:text-red-800 dark:text-red-300 dark:hover:text-red-200" : "bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200";
|
||||
});
|
||||
|
||||
const isTransactionActive = computed(() => !!props.txnSessionId);
|
||||
const canMultiExecute = computed(() => {
|
||||
if (!supportsQueryExecution(props.activeConnection?.db_type)) return false;
|
||||
if (props.activeTab.isExecuting || props.activeTab.isExplaining || props.activeTab.isCancelling) return false;
|
||||
@@ -306,41 +308,6 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ explainAnalyzeTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<!-- Transaction toggle -->
|
||||
<Tooltip v-if="supportsTransaction">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
:class="isTransactionActive || autoCommit === false ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300' : 'text-orange-600/70 hover:bg-orange-500/10 hover:text-orange-700 dark:text-orange-300/70 dark:hover:text-orange-200'"
|
||||
:disabled="activeTab.isExecuting || activeTab.isExplaining"
|
||||
@click="emit('update:autoCommit', autoCommit === false)"
|
||||
>
|
||||
<span class="text-xs font-bold leading-none">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')">
|
||||
@@ -441,6 +408,48 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("toolbar.multiDbExecute") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<div v-if="supportsTransaction" class="ml-1 flex items-center gap-0.5 border-l border-border/60 pl-1" role="group" :aria-label="transactionTooltip">
|
||||
<!-- Transaction toggle -->
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-8 px-1"
|
||||
:class="isManualTransactionMode ? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300' : 'text-orange-600/70 hover:bg-orange-500/10 hover:text-orange-700 dark:text-orange-300/70 dark:hover:text-orange-200'"
|
||||
:disabled="activeTab.isExecuting || activeTab.isExplaining"
|
||||
:aria-label="transactionTooltip"
|
||||
:aria-pressed="isManualTransactionMode"
|
||||
@click="emit('update:autoCommit', autoCommit === false)"
|
||||
>
|
||||
<span class="inline-flex items-center gap-px leading-none" aria-hidden="true">
|
||||
<span class="text-[11px] font-bold">Tx:</span>
|
||||
<span class="inline-flex h-3 min-w-3 items-center justify-center rounded-[3px] border border-current px-px text-[8px] font-extrabold leading-none">{{ transactionModeBadge }}</span>
|
||||
</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" :aria-label="t('toolbar.commit')" @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" :aria-label="t('toolbar.rollback')" @click="emit('rollback')">
|
||||
<RotateCcw class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("toolbar.rollback") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0" />
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const toolbarSource = readFileSync(new URL("../EditorToolbar.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("EditorToolbar transaction mode control", () => {
|
||||
it("shows an A or M badge next to Tx instead of relying on color alone", () => {
|
||||
expect(toolbarSource).toContain('const transactionModeBadge = computed(() => (isManualTransactionMode.value ? "M" : "A"));');
|
||||
expect(toolbarSource).toContain('<span class="text-[11px] font-bold">Tx:</span>');
|
||||
expect(toolbarSource).toContain(">{{ transactionModeBadge }}</span>");
|
||||
});
|
||||
|
||||
it("exposes the transaction mode to assistive technology", () => {
|
||||
expect(toolbarSource).toContain('class="ml-1 flex items-center gap-0.5 border-l border-border/60 pl-1" role="group" :aria-label="transactionTooltip"');
|
||||
expect(toolbarSource).toContain(':aria-label="transactionTooltip"');
|
||||
expect(toolbarSource).toContain(':aria-pressed="isManualTransactionMode"');
|
||||
expect(toolbarSource).toContain(":aria-label=\"t('toolbar.commit')\"");
|
||||
expect(toolbarSource).toContain(":aria-label=\"t('toolbar.rollback')\"");
|
||||
});
|
||||
|
||||
it("places the transaction controls at the end in mode, commit, rollback order", () => {
|
||||
const multiExecuteIndex = toolbarSource.indexOf("@click=\"emit('multiExecute')\"");
|
||||
const transactionToggleIndex = toolbarSource.indexOf("@click=\"emit('update:autoCommit', autoCommit === false)\"");
|
||||
const commitIndex = toolbarSource.indexOf("@click=\"emit('commit')\"");
|
||||
const rollbackIndex = toolbarSource.indexOf("@click=\"emit('rollback')\"");
|
||||
const actionGroupEndIndex = toolbarSource.indexOf('<span class="flex-1 min-w-0" />');
|
||||
|
||||
expect(multiExecuteIndex).toBeGreaterThan(-1);
|
||||
expect(transactionToggleIndex).toBeGreaterThan(multiExecuteIndex);
|
||||
expect(commitIndex).toBeGreaterThan(transactionToggleIndex);
|
||||
expect(rollbackIndex).toBeGreaterThan(commitIndex);
|
||||
expect(actionGroupEndIndex).toBeGreaterThan(rollbackIndex);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { connectionNamespaceCreationTarget, databaseNodeNamespaceCreationTarget
|
||||
import { editableDatabasePropertyGroups, editableSchemaPropertyGroups } from "@/lib/database/databasePropertyEditing";
|
||||
import { buildGetDatabaseCommentSql } from "@/lib/database/dbAdminSql";
|
||||
import {
|
||||
defaultAutoCommitForDbType,
|
||||
isSchemaAware,
|
||||
supportsConnectionScopedQueryExecution,
|
||||
supportsConnectionDatabaseBrowser,
|
||||
@@ -89,6 +90,7 @@ describe("supportsTransaction", () => {
|
||||
it("returns true for supported database types", () => {
|
||||
expect(supportsTransaction("postgres")).toBe(true);
|
||||
expect(supportsTransaction("mysql")).toBe(true);
|
||||
expect(supportsTransaction("oracle")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for unsupported database types", () => {
|
||||
@@ -101,7 +103,6 @@ describe("supportsTransaction", () => {
|
||||
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);
|
||||
@@ -112,6 +113,19 @@ describe("supportsTransaction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("defaultAutoCommitForDbType", () => {
|
||||
it("defaults Oracle query tabs to manual transactions", () => {
|
||||
expect(defaultAutoCommitForDbType("oracle")).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults other databases to auto-commit", () => {
|
||||
expect(defaultAutoCommitForDbType("mysql")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType("postgres")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType("dameng")).toBe(true);
|
||||
expect(defaultAutoCommitForDbType(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("supportsSqlInListPaste", () => {
|
||||
it("allows generic and SQL-like editors", () => {
|
||||
expect(supportsSqlInListPaste(undefined)).toBe(true);
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface SavedOpenTab {
|
||||
whereInput?: string;
|
||||
pinned?: boolean;
|
||||
mode?: QueryTab["mode"];
|
||||
autoCommit?: boolean;
|
||||
mqTenant?: string;
|
||||
mqInitialTab?: QueryTab["mqInitialTab"];
|
||||
nacosNamespace?: string;
|
||||
@@ -117,6 +118,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
||||
...(tab.whereInput !== undefined ? { whereInput: tab.whereInput } : {}),
|
||||
pinned: tab.pinned,
|
||||
mode: tab.mode,
|
||||
...(tab.mode === "query" && tab.autoCommit !== undefined ? { autoCommit: tab.autoCommit } : {}),
|
||||
...(tab.mqTenant !== undefined ? { mqTenant: tab.mqTenant } : {}),
|
||||
...(tab.mqInitialTab !== undefined ? { mqInitialTab: tab.mqInitialTab } : {}),
|
||||
...(tab.nacosNamespace !== undefined ? { nacosNamespace: tab.nacosNamespace } : {}),
|
||||
|
||||
@@ -229,7 +229,7 @@ export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
|
||||
return !!dbType && PG_LIKE_STRUCTURE_TYPES.has(dbType);
|
||||
}
|
||||
|
||||
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql"];
|
||||
const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql", "oracle"];
|
||||
|
||||
/**
|
||||
* Returns true if the given database type supports explicit transaction control
|
||||
@@ -238,3 +238,11 @@ const TRANSACTION_SUPPORTED_TYPES: readonly string[] = ["postgres", "mysql"];
|
||||
export function supportsTransaction(dbType?: string): boolean {
|
||||
return !!dbType && TRANSACTION_SUPPORTED_TYPES.includes(dbType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default auto-commit mode when opening a query tab for the given database type.
|
||||
* Oracle defaults to manual transactions so edits require an explicit Commit.
|
||||
*/
|
||||
export function defaultAutoCommitForDbType(dbType?: string): boolean {
|
||||
return dbType !== "oracle";
|
||||
}
|
||||
@@ -408,6 +408,8 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT t.* FROM SH_SMCVDMS_OVERSEAS_DRSSITB.TT_PLATFORM_CARS t WHERE t.PLATFORM = '轻卡'");
|
||||
|
||||
@@ -461,6 +463,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM CUSTOMERS");
|
||||
|
||||
@@ -494,6 +497,8 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM aa");
|
||||
|
||||
@@ -532,6 +537,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ncdb", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
const sql = "SELECT * FROM VITF_PRODUCT_INFOOA WHERE PK_MATERIAL IN ('1', '2')";
|
||||
|
||||
const execution = store.executeTabSql(tabId, sql);
|
||||
@@ -568,6 +574,8 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
const execution = store.executeTabSql(tabId, "SELECT t.* FROM APP.WIDE_TABLE t");
|
||||
await vi.waitFor(() => expect(executeMulti).toHaveBeenCalled());
|
||||
@@ -618,6 +626,8 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, sql);
|
||||
|
||||
@@ -665,6 +675,8 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabId, true);
|
||||
const tab = store.tabs.find((item) => item.id === tabId)!;
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM APP.PLATFORM_VIEW");
|
||||
@@ -695,6 +707,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT PAYLOAD FROM APP.DOCUMENTS");
|
||||
|
||||
@@ -723,6 +736,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM APP.DOCUMENT_VIEW");
|
||||
|
||||
@@ -752,6 +766,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM DOCUMENTS");
|
||||
|
||||
@@ -780,6 +795,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT NAME FROM APP.CUSTOMERS");
|
||||
|
||||
@@ -817,6 +833,7 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "ORCL", "Query");
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT * FROM CUSTOMERS");
|
||||
|
||||
@@ -959,6 +976,8 @@ describe("queryStore hidden primary key editing", () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const tabId = store.createTab("oracle-1", "XEPDB1", "Query");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabId, true);
|
||||
|
||||
await store.executeTabSql(tabId, "SELECT NAME FROM DBX_HIDDEN_PK_EDIT_TEST");
|
||||
|
||||
|
||||
@@ -386,6 +386,9 @@ describe("queryStore multi-statement errors", () => {
|
||||
const store = useQueryStore();
|
||||
const tabA = store.createTab("oracle-1", "ORCL", "Tab A");
|
||||
const tabB = store.createTab("oracle-1", "ORCL", "Tab B");
|
||||
// Exercise auto-commit execute-multi path (Oracle tabs default to manual TX).
|
||||
store.setAutoCommit(tabA, true);
|
||||
store.setAutoCommit(tabB, true);
|
||||
|
||||
await store.executeTabSql(tabA, "ALTER SESSION SET CURRENT_SCHEMA = REPORTING");
|
||||
expect(store.tabs.find((tab) => tab.id === tabA)?.completionContextVersion).toBe(1);
|
||||
|
||||
@@ -371,7 +371,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
let savedSqlFilesByDatabase = indexSavedSqlFilesByDatabase(savedSqlStore.allFiles);
|
||||
const connections = ref<ConnectionConfig[]>([]);
|
||||
const isDesktop = isTauriRuntime();
|
||||
const activeConnectionId = ref<string | null>(localStorage.getItem(ACTIVE_CONNECTION_STORAGE_KEY));
|
||||
// Prefer a safe read so Node/vitest (missing or partial storage mocks) do not
|
||||
// throw when query tabs resolve connection db_type defaults via this store.
|
||||
const activeConnectionId = ref<string | null>(
|
||||
(() => {
|
||||
try {
|
||||
const storage = globalThis.localStorage;
|
||||
if (!storage || typeof storage.getItem !== "function") return null;
|
||||
return storage.getItem(ACTIVE_CONNECTION_STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})(),
|
||||
);
|
||||
const selectedTreeNodeId = ref<string | null>(null);
|
||||
const selectedTreeNodeIds = ref<string[]>([]);
|
||||
// O(1) membership set — rebuilds only when selectedTreeNodeIds changes.
|
||||
@@ -386,8 +398,14 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const treeClipboard = ref<TreeClipboard | null>(null);
|
||||
|
||||
watch(activeConnectionId, (id) => {
|
||||
if (id) localStorage.setItem(ACTIVE_CONNECTION_STORAGE_KEY, id);
|
||||
else localStorage.removeItem(ACTIVE_CONNECTION_STORAGE_KEY);
|
||||
try {
|
||||
const storage = globalThis.localStorage;
|
||||
if (!storage || typeof storage.setItem !== "function" || typeof storage.removeItem !== "function") return;
|
||||
if (id) storage.setItem(ACTIVE_CONNECTION_STORAGE_KEY, id);
|
||||
else storage.removeItem(ACTIVE_CONNECTION_STORAGE_KEY);
|
||||
} catch {
|
||||
// Ignore storage failures in non-browser / partial mock environments.
|
||||
}
|
||||
});
|
||||
const treeNodes = ref<TreeNode[]>([]);
|
||||
const sidebarDatabaseStorageCache = new Map<string, { expiresAt: number; value: DatabaseStorageInfo[] }>();
|
||||
|
||||
@@ -35,7 +35,7 @@ import { redisCommandResultToQueryResult } from "@/lib/redis/redisQueryResult";
|
||||
import { nextRedisCommandDb } from "@/lib/redis/redisCommandSession";
|
||||
import { isRedisMutatingCommand } from "@/lib/redis/redisCommandTable";
|
||||
import { usesAgentCursorForQuery } from "@/lib/database/databaseDriverManifest";
|
||||
import { supportsClearableQuerySchema } from "@/lib/database/databaseFeatureSupport";
|
||||
import { defaultAutoCommitForDbType, supportsClearableQuerySchema } from "@/lib/database/databaseFeatureSupport";
|
||||
import { canInsertTableRows, canUseKeylessRowPredicate, DBX_ROWID_COLUMN, editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/table/tableEditing";
|
||||
import { TABLE_DATA_EXPORT_PAGE_SIZE } from "@/lib/table/tableDataExport";
|
||||
import { tableMetaForDataTab } from "@/lib/table/tableDataTabMeta";
|
||||
@@ -1509,9 +1509,12 @@ export const useQueryStore = defineStore("query", () => {
|
||||
function applyRestoredOpenTabs(restored: { tabs: QueryTab[]; activeTabId: string | null }) {
|
||||
const connectionStore = useConnectionStore();
|
||||
for (const tab of restored.tabs) {
|
||||
if (tab.mode !== "data") continue;
|
||||
const connection = connectionStore.getConfig(tab.connectionId);
|
||||
if (connection) tab.schema = connectionObjectTreeNodeSchema(connection, tab.database, tab.schema);
|
||||
if (tab.mode === "query" && tab.autoCommit === undefined) {
|
||||
tab.autoCommit = defaultAutoCommitForDbType(connection?.db_type);
|
||||
} else if (tab.mode === "data" && connection) {
|
||||
tab.schema = connectionObjectTreeNodeSchema(connection, tab.database, tab.schema);
|
||||
}
|
||||
}
|
||||
tabs.value = restored.tabs;
|
||||
activeTabId.value = restored.activeTabId;
|
||||
@@ -1657,6 +1660,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
const dbType = useConnectionStore().getConfig(connectionId)?.db_type;
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: title || `query_${tabs.value.length + 1}`,
|
||||
@@ -1671,6 +1675,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode,
|
||||
...(mode === "query" ? { autoCommit: defaultAutoCommitForDbType(dbType) } : {}),
|
||||
};
|
||||
if (mode === "query") tab.originalSql = initialSql ?? "";
|
||||
const activeIndex = options.insertAfterActive ? tabs.value.findIndex((item) => item.id === activeTabId.value) : -1;
|
||||
@@ -1748,6 +1753,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
// File-backed tabs are identified by their full path, not their basename.
|
||||
// Bypassing createTab avoids overwriting another file with the same name.
|
||||
const id = uuid();
|
||||
const dbType = useConnectionStore().getConfig(connectionId)?.db_type;
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: "",
|
||||
@@ -1763,6 +1769,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "query",
|
||||
autoCommit: defaultAutoCommitForDbType(dbType),
|
||||
};
|
||||
tabs.value.push(tab);
|
||||
activeTabId.value = id;
|
||||
@@ -3022,7 +3029,10 @@ export const useQueryStore = defineStore("query", () => {
|
||||
|
||||
function rollbackTabTransaction(tab: QueryTab, options?: { resetAutoCommit?: boolean }) {
|
||||
if (tab.txnSessionId) void rollbackTransaction(tab.id);
|
||||
if (options?.resetAutoCommit) tab.autoCommit = true;
|
||||
if (options?.resetAutoCommit) {
|
||||
const dbType = useConnectionStore().getConfig(tab.connectionId)?.db_type;
|
||||
tab.autoCommit = defaultAutoCommitForDbType(dbType);
|
||||
}
|
||||
tab.txnAutoRolledBack = false;
|
||||
}
|
||||
|
||||
@@ -3161,6 +3171,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
|
||||
const id = uuid();
|
||||
const restoredPosition = restoreSavedSqlEditorPosition(file.id, file.sql);
|
||||
const dbType = useConnectionStore().getConfig(target.connectionId)?.db_type;
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: file.name,
|
||||
@@ -3176,6 +3187,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "query",
|
||||
autoCommit: defaultAutoCommitForDbType(dbType),
|
||||
editorSelection: restoredPosition.selection,
|
||||
editorViewport: restoredPosition.viewport,
|
||||
};
|
||||
@@ -5175,13 +5187,17 @@ export const useQueryStore = defineStore("query", () => {
|
||||
queryExecutionLog("error", "error", { traceId, elapsed: elapsed(), error: e });
|
||||
// Sync connection state if the error indicates a lost connection
|
||||
useConnectionStore().recordConnectionLostError(executionConnectionId ?? tab.connectionId, e);
|
||||
// Handle manual transaction auto-rollback (e.g. deadlock detected by server,
|
||||
// statement error inside a manual transaction, or idle timeout).
|
||||
// Handle manual transaction auto-rollback (idle timeout only for the banner;
|
||||
// other statement failures still clear the session without the 5-minute notice).
|
||||
if (tab.autoCommit === false) {
|
||||
const errMsg: string = e?.message ?? String(e);
|
||||
if (/rolled.?back/i.test(errMsg) || errMsg.includes("已自动回滚")) {
|
||||
const idleTimeout = /5 minutes of inactivity/i.test(errMsg) || errMsg.includes("5 分钟无操作") || errMsg.includes("已自动回滚");
|
||||
if (idleTimeout) {
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = true;
|
||||
} else if (/rolled.?back/i.test(errMsg) || /transaction session not found/i.test(errMsg) || /agent runtime terminated/i.test(errMsg)) {
|
||||
tab.txnSessionId = undefined;
|
||||
tab.txnAutoRolledBack = false;
|
||||
}
|
||||
}
|
||||
const current = findExecutionTab(id);
|
||||
@@ -5881,6 +5897,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
function openResultArchiveTab(archive: DecodedQueryResultArchive): string | undefined {
|
||||
const id = uuid();
|
||||
const title = archive.tab.title.trim() || t("tabs.importedResultArchive");
|
||||
const dbType = useConnectionStore().getConfig(archive.tab.connectionId)?.db_type;
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title,
|
||||
@@ -5897,6 +5914,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "query",
|
||||
autoCommit: defaultAutoCommitForDbType(dbType),
|
||||
};
|
||||
if (!restoreCachedResultPayload(tab, archive.snapshot)) return undefined;
|
||||
const activeRun = tab.resultRuns?.find((run) => run.id === tab.activeResultRunId) ?? tab.resultRuns?.[0];
|
||||
|
||||
Reference in new issue
Block a user