fix(oceanbase-oracle): handle manual transactions on grid save/switch
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user