fix(oceanbase-oracle): handle manual transactions on grid save/switch

This commit is contained in:
efeire authored and GitHub committed 2026-09-20 00:49:33 +08:00
1 parent 30d8b3970a
commit a19a4f595d
6 files changed
+302 -30

No files matched your search

@@ -949,6 +949,7 @@ describe("useDataGridEditor saveChanges reload", () => {
prepareFullReload?: () => void;
customSaveHandler?: { save: ReturnType<typeof vi.fn> };
manualTransactionSessionId?: string;
ensureManualTransactionSession?: () => Promise<string>;
refreshSavedRows?: ReturnType<typeof vi.fn>;
onManualTransactionMutation?: ReturnType<typeof vi.fn>;
connectionId?: string;
@@ -958,6 +959,7 @@ describe("useDataGridEditor saveChanges reload", () => {
) {
const emit = vi.fn();
const currentPage = options.currentPage ?? ref(1);
const ensureManualTransactionSession = ref(options.ensureManualTransactionSession);
const result = ref<{ columns: string[]; rows: CellValue[][] }>({
columns: ["id", "status"],
rows: [
@@ -985,6 +987,7 @@ describe("useDataGridEditor saveChanges reload", () => {
onExecuteSql: computed(() => options.onExecuteSql),
customSaveHandler: computed(() => options.customSaveHandler),
manualTransactionSessionId: computed(() => options.manualTransactionSessionId),
ensureManualTransactionSession: computed(() => ensureManualTransactionSession.value),
onManualTransactionMutation: options.onManualTransactionMutation,
sql: computed(() => undefined),
searchText: ref(""),
@@ -1001,7 +1004,7 @@ describe("useDataGridEditor saveChanges reload", () => {
refreshSavedRows: options.refreshSavedRows,
emit,
});
return { editor, emit, currentPage };
return { editor, emit, currentPage, ensureManualTransactionSession };
}
// https://github.com/t8y2/dbx/issues/8321: without a primary key the row is
@@ -1148,6 +1151,53 @@ describe("useDataGridEditor saveChanges reload", () => {
expect(emit).toHaveBeenCalledWith("reload", undefined, "", undefined, undefined, 100, 0);
});
it("starts a manual transaction before saving an existing result grid", async () => {
const statement = "UPDATE orders_test SET status='shipped' WHERE id=1";
const ensureManualTransactionSession = vi.fn().mockResolvedValue("txn-grid-1");
mocks.prepareDataGridSave.mockResolvedValue({ statements: [statement], rollbackStatements: [] });
mocks.executeInManualTransaction.mockResolvedValue([{ affected_rows: 1 }]);
const { editor, ensureManualTransactionSession: transactionMode } = createSaveTestEditor();
transactionMode.value = ensureManualTransactionSession;
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(ensureManualTransactionSession).toHaveBeenCalledOnce();
expect(mocks.executeInManualTransaction).toHaveBeenCalledWith("txn-grid-1", statement, "app", undefined);
expect(mocks.executeBatch).not.toHaveBeenCalled();
});
it("checks a keyless edit on the newly opened manual transaction session", async () => {
mocks.prepareDataGridSave.mockResolvedValue({
statements: ["UPDATE orders_test SET status='shipped' WHERE status = 'pending'"],
rollbackStatements: [],
keylessGuards: [keylessGuard],
});
mocks.executeInManualTransaction.mockResolvedValueOnce([{ columns: ["matches"], rows: [[1]] }]).mockResolvedValueOnce([{ affected_rows: 1 }]);
const { editor } = createSaveTestEditor({ primaryKeys: [], ensureManualTransactionSession: vi.fn().mockResolvedValue("txn-grid-2") });
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(mocks.executeInManualTransaction.mock.calls[0]?.[0]).toBe("txn-grid-2");
expect(mocks.executeInManualTransaction.mock.calls[0]?.[1]).toBe(keylessGuard.sql);
expect(mocks.executeInManualTransaction.mock.calls[1]?.[0]).toBe("txn-grid-2");
expect(mocks.executeQuery).not.toHaveBeenCalled();
expect(mocks.executeBatch).not.toHaveBeenCalled();
});
it("keeps edits pending if a manual transaction cannot start", async () => {
mocks.prepareDataGridSave.mockResolvedValue({ statements: ["UPDATE orders_test SET status='shipped' WHERE id=1"], rollbackStatements: [] });
const ensureManualTransactionSession = vi.fn().mockRejectedValue(new Error("BEGIN failed"));
const { editor } = createSaveTestEditor({ ensureManualTransactionSession });
editor.dirtyRows.value.set(0, new Map([[1, "shipped"]]));
await editor.saveChanges();
expect(editor.saveError.value).toContain("BEGIN failed");
expect(editor.dirtyRows.value.get(0)?.get(1)).toBe("shipped");
expect(mocks.executeBatch).not.toHaveBeenCalled();
expect(mocks.executeInManualTransaction).not.toHaveBeenCalled();
});
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: [] });
@@ -109,6 +109,7 @@ export interface UseDataGridEditorOptions {
onExecuteSql: ComputedRef<((sql: string) => Promise<void>) | undefined>;
customSaveHandler?: ComputedRef<CustomSaveHandler | undefined>;
manualTransactionSessionId?: ComputedRef<string | undefined>;
ensureManualTransactionSession?: ComputedRef<(() => Promise<string>) | undefined>;
onManualTransactionMutation?: () => void;
sql: ComputedRef<string | undefined>;
searchText: Ref<string>;
@@ -1598,9 +1599,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
// A keyless save can only be trusted once the server confirms that each
// predicate it sends addresses a single physical row; the loaded page cannot
// see rows outside it. Returns the error to fail with, or undefined to allow.
async function verifyKeylessGuards(guards: DataGridSaveGuard[], executionSchema?: string) {
async function verifyKeylessGuards(guards: DataGridSaveGuard[], executionSchema?: string, txnSessionId = manualTransactionSessionId.value) {
if (!guards.length) return undefined;
const txnSessionId = manualTransactionSessionId.value;
// Without a connection to count against there is no way to verify the
// predicate, and an unverified keyless write is exactly what must not run.
if (!hasBackendSaveTarget.value) return KEYLESS_GUARD_UNVERIFIED_ERROR;
@@ -1948,8 +1948,19 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
return;
}
const rollbackStmts = preparedSave?.rollbackStatements ?? [];
let txnSessionId = manualTransactionSessionId.value;
if (options.ensureManualTransactionSession?.value && hasBackendSaveTarget.value) {
try {
txnSessionId = await options.ensureManualTransactionSession.value();
if (!txnSessionId) throw new Error("Manual transaction session was not initialized");
} catch (error) {
saveError.value = normalizeDataGridSaveError(databaseType.value, error);
await finishInterruptedSaveChanges(snapshot);
return;
}
}
try {
const guardError = await verifyKeylessGuards(preparedSave?.keylessGuards ?? [], preparedSave?.executionSchema);
const guardError = await verifyKeylessGuards(preparedSave?.keylessGuards ?? [], preparedSave?.executionSchema, txnSessionId);
if (guardError) {
saveError.value = guardError;
await finishInterruptedSaveChanges(snapshot);
@@ -1991,10 +2002,10 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
statements: stmts,
rollbackStatements: rollbackStmts,
});
if (manualTransactionSessionId.value && hasBackendSaveTarget.value) {
if (txnSessionId && hasBackendSaveTarget.value) {
options.onManualTransactionMutation?.();
try {
const results = await api.executeInManualTransaction(manualTransactionSessionId.value, stmts.join(";\n"), database.value ?? "", preparedSave?.executionSchema);
const results = await api.executeInManualTransaction(txnSessionId, stmts.join(";\n"), database.value ?? "", preparedSave?.executionSchema);
apiResult = {
affected_rows: results.reduce((total, result) => total + (result.affected_rows ?? 0), 0),
};
@@ -2038,7 +2049,7 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
applyDirtyRowsToResult(snapshot);
options.onResultPayloadMutated?.();
let savedRowsRefreshed = false;
if (!joinedWriteTargets.value?.length && !manualTransactionSessionId.value && !shouldReloadAfterSave && snapshot.dirtyRows.size > 0 && options.refreshSavedRows) {
if (!joinedWriteTargets.value?.length && !txnSessionId && !shouldReloadAfterSave && snapshot.dirtyRows.size > 0 && options.refreshSavedRows) {
try {
savedRowsRefreshed = await options.refreshSavedRows({
dirtyRows: snapshot.dirtyRows,