Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,46 @@ test('web table browse retains execution params across consecutive page requests
assert.equal(secondPage.executeSqlParams?.pageSize, 5000);
});

test('a cancelled table browse never publishes its buffered partial rows', () => {
const params = { sql: BASE_SQL, dataSourceId: 42, pageNo: 1, pageSize: 50_000 };
test('a failed table browse page preserves the confirmed result and reports the backend error', () => {
const confirmedResult = result({
uuid: 'confirmed-result',
dataList: [[{ value: 'confirmed-row' }]] as any,
pageNo: 1,
});
const params = { sql: BASE_SQL, dataSourceId: 42, pageNo: 2, pageSize: 1000 };
const requestSequence = 8;

const transition = reduceViewTablePagingEvent(
createViewTablePagingState(requestSequence, params, confirmedResult),
event(
'resultFinished',
result({
success: false,
message: 'Unknown column status',
dataList: [],
pageNo: 2,
}),
),
requestSequence,
);

assert.equal(transition.completedResult, undefined, 'a failed page must not replace confirmed rows');
assert.equal(transition.errorMessage, 'Unknown column status');
assert.equal(transition.state.errorMessage, 'Unknown column status');
assert.deepEqual(confirmedResult.dataList, [[{ value: 'confirmed-row' }]]);
});

test('a cancelled table browse republishes the confirmed page without buffered partial rows', () => {
const params = { sql: BASE_SQL, dataSourceId: 42, pageNo: 2, pageSize: 50_000 };
const confirmedResult = result({
uuid: 'confirmed-result',
dataList: [[{ value: 'confirmed-row' }]] as any,
pageNo: 1,
pageSize: 1000,
});
const requestSequence = 9;
let transition = reduceViewTablePagingEvent(
createViewTablePagingState(requestSequence, params),
createViewTablePagingState(requestSequence, params, confirmedResult),
event('rows', result({ dataList: [[{ value: 'partial' }]] as any })),
requestSequence,
);
Expand All @@ -223,7 +258,10 @@ test('a cancelled table browse never publishes its buffered partial rows', () =>
event('cancelled', result()),
requestSequence,
);
assert.equal(transition.completedResult, undefined, 'cancellation leaves the visible completed result untouched');
assert.notEqual(transition.completedResult, confirmedResult, 'rollback must publish a new object');
assert.equal(transition.completedResult?.pageNo, 1);
assert.equal(transition.completedResult?.pageSize, 1000);
assert.deepEqual(transition.completedResult?.dataList, [[{ value: 'confirmed-row' }]]);
});

test('a failed filter keeps the mounted table and allows a corrected query to replace it', () => {
Expand All @@ -236,7 +274,7 @@ test('a failed filter keeps the mounted table and allows a corrected query to re
);

assert.equal(failed.completedResult, undefined, 'a SQL error must not replace and unmount the table');
assert.equal(failed.state.result?.message, 'Invalid WHERE condition', 'the request retains the error to report');
assert.equal(failed.state.errorMessage, 'Invalid WHERE condition', 'the request retains the error to report');

const correctedParams = { ...failedParams, sql: `${BASE_SQL} WHERE id = 1` };
const corrected = reduceViewTablePagingEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const ViewTable = memo<IProps>((props) => {
if (executeSqlParams.dataSourceId == null || !executeSqlParams.sql) {
return;
}
return executePage(executeSqlParams);
return executePage(executeSqlParams, _resultData);
},
[executePage],
);
Expand Down
25 changes: 19 additions & 6 deletions chat2db-community-client/src/hooks/useViewTablePaging.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useCallback, useRef, useState } from 'react';
import type { IExecuteSqlParams } from '@/typings';
import type { IExecuteSqlParams, IManageResultData } from '@/typings';
import type { SqlExecutionEvent } from '@/service/sqlExecutionStream';
import useSqlExecutor from './useSqlExecutor';
import {
createViewTablePagingState,
getViewTablePagingErrorMessage,
normalizeViewTablePageResults,
reduceViewTablePagingEvent,
type ViewTablePagingState,
Expand All @@ -12,13 +13,18 @@ import {
export default function useViewTablePaging() {
const requestSequenceRef = useRef<number>();
const paramsRef = useRef<IExecuteSqlParams>();
const confirmedResultRef = useRef<IManageResultData>();
const pagingStateRef = useRef<ViewTablePagingState>();
const [resultData, setResultData] = useState<ViewTablePagingState['result']>();

const handleRequestStart = useCallback((requestSequence: number) => {
requestSequenceRef.current = requestSequence;
if (paramsRef.current) {
pagingStateRef.current = createViewTablePagingState(requestSequence, paramsRef.current);
pagingStateRef.current = createViewTablePagingState(
requestSequence,
paramsRef.current,
confirmedResultRef.current,
);
}
}, []);

Expand All @@ -40,13 +46,20 @@ export default function useViewTablePaging() {
});

const executePage = useCallback(
(params: IExecuteSqlParams) => {
(params: IExecuteSqlParams, confirmedResult: IManageResultData) => {
paramsRef.current = params;
confirmedResultRef.current = confirmedResult;
return executeSQL(params).then((data) => {
const streamState = pagingStateRef.current;
if (streamState?.params === params && streamState.errorMessage) {
throw new Error(streamState.errorMessage);
}
const normalizedData = normalizeViewTablePageResults(data, params);
const pageResult = normalizedData[0] || pagingStateRef.current?.result;
if (pageResult?.success === false) {
throw new Error(pageResult.message);
const responseError = normalizedData
.map(getViewTablePagingErrorMessage)
.find((message): message is string => !!message);
if (responseError) {
throw new Error(responseError);
}
if (normalizedData.length) {
setResultData(normalizedData[0]);
Expand Down
37 changes: 34 additions & 3 deletions chat2db-community-client/src/hooks/viewTablePagingModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ export type ViewTableStreamEventType = 'resultStarted' | 'rows' | 'resultFinishe
export interface ViewTablePagingState {
requestSequence: number;
params: IExecuteSqlParams;
confirmedResult?: IManageResultData;
result?: IManageResultData;
errorMessage?: string;
}

export interface ViewTablePagingTransition {
state: ViewTablePagingState;
completedResult?: IManageResultData;
errorMessage?: string;
}

function isResultEvent(eventType: SqlExecutionEvent['eventType']): eventType is ViewTableStreamEventType {
Expand Down Expand Up @@ -44,8 +47,12 @@ function mergeResultEvent(
};
}

export function createViewTablePagingState(requestSequence: number, params: IExecuteSqlParams) {
return { requestSequence, params } satisfies ViewTablePagingState;
export function createViewTablePagingState(
requestSequence: number,
params: IExecuteSqlParams,
confirmedResult?: IManageResultData,
) {
return { requestSequence, params, confirmedResult } satisfies ViewTablePagingState;
}

export function normalizeViewTablePageResults(
Expand All @@ -55,19 +62,43 @@ export function normalizeViewTablePageResults(
return processResultDataList(results, params);
}

export function getViewTablePagingErrorMessage(result: IManageResultData) {
if (result.success !== false) {
return undefined;
}
return result.message?.trim() || result.description?.trim() || 'SQL execution failed';
}

export function reduceViewTablePagingEvent(
state: ViewTablePagingState,
event: SqlExecutionEvent,
requestSequence: number,
): ViewTablePagingTransition {
if (state.requestSequence !== requestSequence || !isResultEvent(event.eventType)) {
if (state.requestSequence !== requestSequence) {
return { state };
}
if (event.eventType === 'cancelled') {
const completedResult = state.confirmedResult ? { ...state.confirmedResult } : undefined;
return {
state: { ...state, result: undefined, errorMessage: undefined },
completedResult,
};
}
if (!isResultEvent(event.eventType)) {
return { state };
}

const eventResult = normalizeViewTablePageResults([event.message], state.params)[0];
if (!eventResult) {
return { state };
}
const errorMessage = getViewTablePagingErrorMessage(eventResult);
if (errorMessage) {
return {
state: { ...state, result: undefined, errorMessage },
errorMessage,
};
}

const result = mergeResultEvent(state.result, eventResult, event.eventType);
const nextState = { ...state, result };
Expand Down
Loading