mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 04:58:41 +00:00
fix(editor): route grid edits through manual txn session
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user