LCOV - code coverage report
Current view: top level - dashql-app/src/views/query_result - use_query_result_search.ts Coverage Total Hit
Test: DashQL 915828b Lines: 94.4 % 126 119
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from 'react';
       2              : 
       3              : import {
       4              :     COLUMN_SEARCH_SUCCEEDED,
       5              :     RESULT_SEARCH_FAILED,
       6              :     SET_RESULT_SEARCH_PATTERN,
       7              :     type ComputationAction,
       8              :     type TableComputationState,
       9              : } from '../../compute/computation_state.js';
      10              : import {
      11              :     buildColumnSearchSQL,
      12              :     collectSearchableResultColumns,
      13              :     parseColumnSearchMatches,
      14              : } from '../../compute/sql/query_result_search_sql.js';
      15              : import type { Dispatch } from '../../utils/variant.js';
      16              : import { useLogger } from '../../platform/logger/logger_provider.js';
      17              : import { stringifyError } from '../../platform/logger/logger.js';
      18              : import { useDataFrameRegistry } from '../../compute/computation_registry.js';
      19              : import { scheduleTask } from '../../compute/computation_tasks.js';
      20              : import { DATA_SEARCH_TASK } from '../../compute/computation_types.js';
      21              : 
      22            1 : const LOG_CTX = 'query_result_search';
      23              : 
      24              : interface SearchExecution {
      25              :     requestId: number;
      26              :     sourceDataFrame: object;
      27              :     abort: AbortController;
      28              : }
      29              : 
      30              : interface SearchExecutions {
      31              :     columns: SearchExecution | null;
      32              :     data: SearchExecution | null;
      33              : }
      34              : 
      35            1 : const executionsByDispatch = new WeakMap<object, Map<number, SearchExecutions>>();
      36              : 
      37            1 : function getExecutions(dispatch: Dispatch<ComputationAction>, tableId: number): SearchExecutions {
      38            1 :     const owner = dispatch as object;
      39            1 :     let executions = executionsByDispatch.get(owner);
      40            1 :     if (executions == null) {
      41            1 :         executions = new Map();
      42            1 :         executionsByDispatch.set(owner, executions);
      43            1 :     }
      44            1 :     let tableExecutions = executions.get(tableId);
      45            1 :     if (tableExecutions == null) {
      46            1 :         tableExecutions = { columns: null, data: null };
      47            1 :         executions.set(tableId, tableExecutions);
      48            1 :     }
      49            1 :     return tableExecutions;
      50            1 : }
      51              : 
      52            1 : export function useQueryResultSearch(
      53            1 :     table: TableComputationState | null,
      54            1 :     dispatch: Dispatch<ComputationAction>,
      55            1 : ) {
      56            1 :     const logger = useLogger();
      57            1 :     const dataFrameMemory = useDataFrameRegistry();
      58            1 :     const columns = React.useMemo(
      59            1 :         () => collectSearchableResultColumns(table?.columnGroups ?? []),
      60            1 :         [table?.columnGroups],
      61            1 :     );
      62              : 
      63            1 :     React.useEffect(() => {
      64            1 :         if (table == null || table.dataFrame == null) return;
      65            1 :         const search = table.columnSearch;
      66            1 :         const executions = getExecutions(dispatch, table.tableId);
      67            1 :         if (search.requestedPattern.length === 0) {
      68            1 :             executions.columns?.abort.abort();
      69            1 :             executions.columns = null;
      70            1 :             return;
      71            1 :         }
      72            1 :         if (search.requestId === search.appliedRequestId) return;
      73            1 :         if (table.dataTableLifetime.signal.aborted) return;
      74              : 
      75            1 :         const sql = buildColumnSearchSQL(columns, search.requestedPattern);
      76            1 :         if (sql == null) {
      77            1 :             dispatch({
      78            1 :                 type: COLUMN_SEARCH_SUCCEEDED,
      79            1 :                 value: [table.tableId, table.dataFrame, search.requestId, []],
      80            1 :             });
      81            1 :             return;
      82            1 :         }
      83              : 
      84            1 :         if (executions.columns?.requestId === search.requestId && executions.columns.sourceDataFrame === table.dataFrame) return;
      85            1 :         executions.columns?.abort.abort();
      86            1 :         const execution = { requestId: search.requestId, sourceDataFrame: table.dataFrame, abort: new AbortController() };
      87            1 :         executions.columns = execution;
      88            1 :         const abortForTableLifetime = () => execution.abort.abort(table.dataTableLifetime.signal.reason);
      89            1 :         table.dataTableLifetime.signal.addEventListener('abort', abortForTableLifetime, { once: true });
      90            1 :         dataFrameMemory.acquire(table.dataFrame);
      91            1 :         void (async () => {
      92            1 :             try {
      93            1 :                 const result = await table.dataFrame!.withConnection(async connection => await connection.query(sql, execution.abort.signal));
      94            1 :                 dispatch({
      95            1 :                     type: COLUMN_SEARCH_SUCCEEDED,
      96            1 :                     value: [table.tableId, table.dataFrame!, search.requestId, parseColumnSearchMatches(result)],
      97            1 :                 });
      98            1 :             } catch (error) {
      99            1 :                 if (execution.abort.signal.aborted) return;
     100            1 :                 logger.warn('Column search failed', { error: stringifyError(error) }, LOG_CTX);
     101            1 :                 dispatch({
     102            1 :                     type: RESULT_SEARCH_FAILED,
     103            1 :                     value: [table.tableId, table.dataFrame!, 'columns', search.requestId, stringifyError(error)],
     104            1 :                 });
     105            1 :             } finally {
     106            1 :                 table.dataTableLifetime.signal.removeEventListener('abort', abortForTableLifetime);
     107            1 :                 dataFrameMemory.release(table.dataFrame);
     108            1 :                 if (executions.columns === execution) executions.columns = null;
     109            1 :             }
     110            1 :         })();
     111            1 :     }, [columns, dataFrameMemory, dispatch, logger, table?.columnSearch?.requestId, table?.dataFrame, table?.tableId]);
     112              : 
     113            1 :     React.useEffect(() => {
     114            1 :         if (table == null || table.dataFrame == null || table.rowNumberColumnName == null) return;
     115            1 :         const search = table.dataSearch;
     116            1 :         const executions = getExecutions(dispatch, table.tableId);
     117            1 :         if (search.requestedPattern.length === 0) {
     118            1 :             executions.data?.abort.abort();
     119            1 :             executions.data = null;
     120            1 :             return;
     121            1 :         }
     122            1 :         if (search.requestId === search.appliedRequestId) return;
     123            1 :         if (table.dataTableLifetime.signal.aborted) return;
     124              : 
     125            1 :         if (executions.data?.requestId === search.requestId && executions.data.sourceDataFrame === table.dataFrame) return;
     126            1 :         executions.data?.abort.abort();
     127            1 :         const execution = { requestId: search.requestId, sourceDataFrame: table.dataFrame, abort: new AbortController() };
     128            1 :         executions.data = execution;
     129            1 :         const abortForTableLifetime = () => execution.abort.abort(table.dataTableLifetime.signal.reason);
     130            1 :         table.dataTableLifetime.signal.addEventListener('abort', abortForTableLifetime, { once: true });
     131            1 :         void (async () => {
     132            1 :             try {
     133            1 :                 await scheduleTask(DATA_SEARCH_TASK, {
     134            1 :                     tableId: table.tableId,
     135            1 :                     tableVersion: table.version,
     136            1 :                     inputDataFrame: table.dataFrame!,
     137            1 :                     rowNumberColumnName: table.rowNumberColumnName!,
     138            1 :                     columns,
     139            1 :                     pattern: search.requestedPattern,
     140            1 :                     requestId: search.requestId,
     141            1 :                     abortController: execution.abort,
     142            1 :                 }, dispatch);
     143            0 :             } catch (error) {
     144            0 :                 if (!execution.abort.signal.aborted) {
     145            0 :                     logger.warn('Data search failed', { error: stringifyError(error) }, LOG_CTX);
     146              :                 }
     147            0 :             } finally {
     148            0 :                 table.dataTableLifetime.signal.removeEventListener('abort', abortForTableLifetime);
     149            0 :                 if (executions.data === execution) executions.data = null;
     150            0 :             }
     151            1 :         })();
     152            1 :     }, [columns, dispatch, logger, table?.dataSearch?.requestId, table?.dataFrame, table?.rowNumberColumnName, table?.tableId, table?.version]);
     153              : 
     154            1 :     const setColumnPattern = React.useCallback((pattern: string) => {
     155            1 :         if (table != null) {
     156            1 :             dispatch({ type: SET_RESULT_SEARCH_PATTERN, value: [table.tableId, 'columns', pattern] });
     157              :         }
     158            1 :     }, [dispatch, table?.tableId]);
     159            1 :     const setDataPattern = React.useCallback((pattern: string) => {
     160            1 :         if (table != null) {
     161            1 :             dispatch({ type: SET_RESULT_SEARCH_PATTERN, value: [table.tableId, 'data', pattern] });
     162              :         }
     163            1 :     }, [dispatch, table?.tableId]);
     164              : 
     165            1 :     return {
     166            1 :         columnSearch: table?.columnSearch ?? null,
     167            1 :         dataSearch: table?.dataSearch ?? null,
     168            1 :         setColumnPattern,
     169            1 :         setDataPattern,
     170            1 :     };
     171            1 : }
        

Generated by: LCOV version 2.0-1