fix(editor): route grid edits through manual txn session

This commit is contained in:
zipg
2026-09-03 10:27:41 +08:00
committed by GitHub
parent 321674c4dc
commit e2eb481b8a
6 changed files with 95 additions and 4 deletions
@@ -515,6 +515,8 @@ interface DataGridProps {
}>;
exportFileBaseName?: string;
customSaveHandler?: import("@/composables/useDataGridEditor").CustomSaveHandler;
manualTransactionSessionId?: string;
onManualTransactionMutation?: () => void;
mongoUpdateTarget?: MongoCopyUpdateTarget;
queryEditabilityReason?: QueryEditabilityReason;
allowInsertRows?: boolean;
@@ -4268,6 +4270,8 @@ const editor = useDataGridEditor({
canEditExistingRows,
onExecuteSql: computed(() => props.onExecuteSql),
customSaveHandler: computed(() => props.customSaveHandler),
manualTransactionSessionId: computed(() => props.manualTransactionSessionId),
onManualTransactionMutation: () => props.onManualTransactionMutation?.(),
sql: computed(() => props.sql),
searchText,
whereFilterInput,
@@ -1794,6 +1794,8 @@ defineExpose({
:custom-save-handler="mongoQueryResultSaveHandler"
:mongo-update-target="mongoQueryResultSaveHandler && activeTab.result.mongo_copy_documents?.length === activeTab.result.rows.length ? activeTab.mongoEditTarget : undefined"
:query-editability-reason="activeTab.queryEditabilityReason"
:manual-transaction-session-id="activeTab.txnSessionId"
:on-manual-transaction-mutation="() => queryStore.markManualTransactionDirty(activeTab.id)"
:allow-insert-rows="activeTab.queryAnalysis?.allowInsert ?? activeTab.queryAnalysis?.allowInsertDelete !== false"
:allow-delete-rows="activeTab.queryAnalysis?.allowDelete ?? activeTab.queryAnalysis?.allowInsertDelete !== false"
context="results"
@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
executeConditionalUpdate: vi.fn(),
cancelConditionalUpdate: vi.fn(),
executeInTransaction: vi.fn(),
executeInManualTransaction: vi.fn(),
addHistory: vi.fn(),
}));
@@ -19,6 +20,7 @@ vi.mock("@/lib/backend/api", () => ({
executeConditionalUpdate: mocks.executeConditionalUpdate,
cancelConditionalUpdate: mocks.cancelConditionalUpdate,
executeInTransaction: mocks.executeInTransaction,
executeInManualTransaction: mocks.executeInManualTransaction,
unlockConnectionWrites: vi.fn(),
lockConnectionWrites: vi.fn(),
connectionWriteUnlockState: vi.fn().mockResolvedValue(0),
@@ -586,11 +588,21 @@ describe("useDataGridEditor saveChanges reload", () => {
mocks.executeConditionalUpdate.mockReset();
mocks.cancelConditionalUpdate.mockReset();
mocks.executeInTransaction.mockReset();
mocks.executeInManualTransaction.mockReset();
mocks.addHistory.mockReset();
mocks.getConfig.mockReset();
});
function createSaveTestEditor(options: { currentPage?: Ref<number>; prepareFullReload?: () => void; customSaveHandler?: { save: ReturnType<typeof vi.fn> } } = {}) {
function createSaveTestEditor(
options: {
currentPage?: Ref<number>;
prepareFullReload?: () => void;
customSaveHandler?: { save: ReturnType<typeof vi.fn> };
manualTransactionSessionId?: string;
refreshSavedRows?: ReturnType<typeof vi.fn>;
onManualTransactionMutation?: ReturnType<typeof vi.fn>;
} = {},
) {
const emit = vi.fn();
const currentPage = options.currentPage ?? ref(1);
const result = ref<{ columns: string[]; rows: CellValue[][] }>({
@@ -617,6 +629,8 @@ describe("useDataGridEditor saveChanges reload", () => {
sourceColumns: computed(() => undefined),
onExecuteSql: computed(() => undefined),
customSaveHandler: computed(() => options.customSaveHandler),
manualTransactionSessionId: computed(() => options.manualTransactionSessionId),
onManualTransactionMutation: options.onManualTransactionMutation,
sql: computed(() => undefined),
searchText: ref(""),
whereFilterInput: ref(""),
@@ -629,6 +643,7 @@ describe("useDataGridEditor saveChanges reload", () => {
cacheKey: computed(() => undefined),
getRowItem: () => undefined,
prepareFullReload: options.prepareFullReload,
refreshSavedRows: options.refreshSavedRows,
emit,
});
return { editor, emit, currentPage };
@@ -647,6 +662,39 @@ describe("useDataGridEditor saveChanges reload", () => {
expect(emit).toHaveBeenCalledWith("reload", undefined, "", undefined, undefined, 100, 0);
});
it("saves query-result edits through the active manual transaction session", async () => {
const statement = "UPDATE orders_test SET status='shipped' WHERE id=1";
const refreshSavedRows = vi.fn().mockResolvedValue(true);
const onManualTransactionMutation = vi.fn();
mocks.prepareDataGridSave.mockResolvedValue({ statements: [statement], rollbackStatements: [] });
mocks.executeInManualTransaction.mockResolvedValue([{ affected_rows: 1 }]);
const { editor, emit } = createSaveTestEditor({ manualTransactionSessionId: "txn-session-1", refreshSavedRows, onManualTransactionMutation });
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(mocks.executeInManualTransaction).toHaveBeenCalledWith("txn-session-1", statement, "app", undefined);
expect(onManualTransactionMutation).toHaveBeenCalledTimes(1);
expect(mocks.executeBatch).not.toHaveBeenCalled();
expect(refreshSavedRows).not.toHaveBeenCalled();
expect(emit).toHaveBeenCalledWith("reload", undefined, "", undefined, undefined, 100, 0);
});
it("marks the manual transaction dirty before a result-grid mutation can fail", async () => {
const onManualTransactionMutation = vi.fn();
mocks.prepareDataGridSave.mockResolvedValue({ statements: ["UPDATE orders_test SET status='shipped' WHERE id=1"], rollbackStatements: [] });
mocks.executeInManualTransaction.mockRejectedValue(new Error("Query timed out"));
const { editor } = createSaveTestEditor({ manualTransactionSessionId: "txn-session-1", onManualTransactionMutation });
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(onManualTransactionMutation).toHaveBeenCalledTimes(1);
expect(editor.saveError.value).toContain("Query timed out");
});
it("executes a conditional update immediately, records affected rows, and reloads", async () => {
mocks.getConfig.mockReturnValue({ id: "connection-1", name: "Local MySQL", db_type: "mysql" });
mocks.executeConditionalUpdate.mockResolvedValue({ affected_rows: 7 });
@@ -103,6 +103,8 @@ export interface UseDataGridEditorOptions {
canEditExistingRows?: ComputedRef<boolean>;
onExecuteSql: ComputedRef<((sql: string) => Promise<void>) | undefined>;
customSaveHandler?: ComputedRef<CustomSaveHandler | undefined>;
manualTransactionSessionId?: ComputedRef<string | undefined>;
onManualTransactionMutation?: () => void;
sql: ComputedRef<string | undefined>;
searchText: Ref<string>;
whereFilterInput: Ref<string>;
@@ -219,6 +221,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
canEditExistingRows = computed(() => true),
onExecuteSql,
customSaveHandler,
manualTransactionSessionId = computed(() => undefined),
sql,
searchText,
orderByInput,
@@ -1818,8 +1821,19 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
statements: stmts,
rollbackStatements: rollbackStmts,
});
if (useTransaction.value && stmts.length > 1 && hasBackendSaveTarget.value) {
if (manualTransactionSessionId.value && hasBackendSaveTarget.value) {
options.onManualTransactionMutation?.();
try {
const results = await api.executeInManualTransaction(manualTransactionSessionId.value, stmts.join(";\n"), database.value ?? "", preparedSave?.executionSchema);
apiResult = {
affected_rows: results.reduce((total, result) => total + (result.affected_rows ?? 0), 0),
};
} catch (e: any) {
saveError.value = await recordFailedDataGridHistory(stmts, rollbackStmts, start, snapshot, e);
await finishInterruptedSaveChanges(snapshot);
return;
}
} else if (useTransaction.value && stmts.length > 1 && hasBackendSaveTarget.value) {
try {
apiResult = await api.executeInTransaction(connectionId.value!, database.value ?? "", stmts, preparedSave?.executionSchema);
} catch (e: any) {
@@ -1854,7 +1868,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
applyDirtyRowsToResult(snapshot);
options.onResultPayloadMutated?.();
let savedRowsRefreshed = false;
if (!shouldReloadAfterSave && snapshot.dirtyRows.size > 0 && options.refreshSavedRows) {
if (!manualTransactionSessionId.value && !shouldReloadAfterSave && snapshot.dirtyRows.size > 0 && options.refreshSavedRows) {
try {
savedRowsRefreshed = await options.refreshSavedRows({
dirtyRows: snapshot.dirtyRows,
@@ -161,6 +161,20 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
expect(tab.oracleTxnPossiblyDirty).toBe(true);
});
it("marks an Oracle manual session dirty when the result grid dispatches a mutation", async () => {
mocks.executeInManualTransaction.mockResolvedValue(cleanSelect());
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = await setupManualTab(store);
await store.executeTabSql(tabId, "SELECT * FROM EMP");
store.markManualTransactionDirty(tabId);
const tab = store.tabs.find((item) => item.id === tabId)!;
expect(tab.oracleTxnPossiblyDirty).toBe(true);
});
it("never clears the sticky state after a later read (UPDATE -> SELECT)", async () => {
mocks.executeInManualTransaction.mockResolvedValueOnce(dirtyUpdate()).mockResolvedValueOnce(cleanSelect());
@@ -332,6 +346,7 @@ describe("queryStore Oracle manual-transaction sticky state", () => {
store.setAutoCommit(tabId, false);
await store.executeTabSql(tabId, "SELECT 1");
store.markManualTransactionDirty(tabId);
const tab = store.tabs.find((item) => item.id === tabId)!;
// No classificationSql was sent and no Oracle sticky state exists.
+8
View File
@@ -3429,6 +3429,13 @@ export const useQueryStore = defineStore("query", () => {
}
}
function markManualTransactionDirty(id: string) {
const tab = tabs.value.find((item) => item.id === id);
if (!tab?.txnSessionId) return;
const dbType = effectiveDatabaseTypeForConnection(useConnectionStore().getConfig(tab.connectionId));
if (dbType === "oracle") tab.oracleTxnPossiblyDirty = true;
}
/** Reset only the Oracle sticky-dirty bit. Used when a session continues but
* the old dirty state must be discarded (e.g. idle-expiry recovery where the
* replacement session starts fresh). Full session cleanup goes through
@@ -7101,6 +7108,7 @@ export const useQueryStore = defineStore("query", () => {
updateObjectBrowserViewport,
updateNacosConfigEditorViewport,
setAutoCommit,
markManualTransactionDirty,
commitTransaction,
rollbackTransaction,
renameTab,