LCOV - code coverage report
Current view: top level - dashql-app/src/connections/catalog - catalog_update_state.tsx Coverage Total Hit
Test: DashQL d805a4b Lines: 98.8 % 86 85
Test Date: 2026-11-09 20:22:13

            Line data    Source code
       1              : import {
       2              :     CATALOG_UPDATE_CANCELLED,
       3              :     CATALOG_UPDATE_FAILED,
       4              :     CATALOG_UPDATE_REGISTER_QUERY,
       5              :     CATALOG_UPDATE_PARTIALLY_SUCCEEDED,
       6              :     CATALOG_UPDATE_SUCCEEDED,
       7              :     CatalogAction,
       8              :     ConnectionState,
       9              :     RELATIONS_TOUCHED,
      10              :     UPDATE_CATALOG,
      11              : } from '../connection_state.js';
      12              : import type { RelationName } from './catalog_sql_generator.js';
      13              : 
      14            1 : export enum CatalogUpdateVariant {
      15            1 :     FULL_CATALOG_REFRESH,
      16              :     /// Ask the connection about the relations that executed statements touched
      17            1 :     RELATION_REFRESH,
      18            1 : }
      19              : 
      20            1 : export enum CatalogUpdateTaskStatus {
      21            1 :     STARTED = 0,
      22            1 :     SUCCEEDED = 1,
      23            1 :     FAILED = 2,
      24            1 :     CANCELLED = 3,
      25            1 :     PARTIALLY_SUCCEEDED = 4,
      26            1 : }
      27              : export const CATALOG_UPDATE_TASK_STATUS_NAMES: string[] = [
      28              :     "Started",
      29              :     "Succeeded",
      30              :     "Failed",
      31              :     "Cancelled",
      32              :     "Partially Succeeded",
      33              : ];
      34              : 
      35              : export interface CatalogUpdateTaskState {
      36              :     /// The task key
      37              :     taskId: number;
      38              :     /// The catalog update variant
      39              :     taskVariant: CatalogUpdateVariant;
      40              :     /// The status
      41              :     status: CatalogUpdateTaskStatus;
      42              :     /// The cancellation signal
      43              :     cancellation: AbortController;
      44              :     /// The queries
      45              :     queries: number[];
      46              :     /// The loading error (if any)
      47              :     error: Error | null;
      48              :     /// The time at which the loading started (if any)
      49              :     startedAt: Date | null;
      50              :     /// The time at which the loading finishe (if any)
      51              :     finishedAt: Date | null;
      52              :     /// The relations of executed statements that the task answers
      53              :     touchedRelations?: RelationName[];
      54              : }
      55              : 
      56              : /// The attempts to answer the relations of executed statements before the catalog counts as out of date
      57              : export const MAX_TOUCHED_ATTEMPTS = 3;
      58              : 
      59              : /// Might the catalog miss what executed statements did, since asking the connection failed?
      60            1 : export function isCatalogOutOfDate(connection: ConnectionState): boolean {
      61            1 :     return connection.catalogUpdates.touchedFailures >= MAX_TOUCHED_ATTEMPTS;
      62            1 : }
      63              : 
      64              : /// Merge relation names, every relation once
      65            1 : export function mergeRelationNames(into: RelationName[], names: RelationName[]): RelationName[] {
      66            1 :     const out = [...into];
      67            1 :     for (const name of names) {
      68            1 :         if (!out.some(r => r.databaseName === name.databaseName && r.schemaName === name.schemaName && r.tableName === name.tableName)) out.push(name);
      69              :     }
      70            1 :     return out;
      71            1 : }
      72              : 
      73              : /// The relations that nothing has answered about yet, also those of a running update
      74            1 : export function pendingRelations(connection: ConnectionState): RelationName[] {
      75            1 :     let pending = connection.catalogUpdates.touchedRelations;
      76            1 :     for (const task of connection.catalogUpdates.tasksRunning.values()) {
      77            0 :         pending = mergeRelationNames(pending, task.touchedRelations ?? []);
      78              :     }
      79            1 :     return pending;
      80            1 : }
      81              : 
      82              : /// What a task that didn't answer gives back
      83            1 : function returnTouched(state: ConnectionState, update: CatalogUpdateTaskState, failed: boolean) {
      84            1 :     const touched = (update.touchedRelations?.length ?? 0) > 0;
      85            1 :     return {
      86            1 :         touchedRelations: mergeRelationNames(state.catalogUpdates.touchedRelations, update.touchedRelations ?? []),
      87            1 :         touchedFailures: state.catalogUpdates.touchedFailures + (failed && touched ? 1 : 0),
      88            1 :     };
      89            1 : }
      90              : 
      91            1 : export function isCatalogRefreshRunning(connection: ConnectionState | null): boolean {
      92            1 :     if (connection == null) return false;
      93            1 :     const refreshId = connection.catalogUpdates.currentFullRefresh;
      94            1 :     return refreshId != null && connection.catalogUpdates.tasksRunning.has(refreshId);
      95            1 : }
      96              : 
      97            1 : export function reduceCatalogAction(state: ConnectionState, action: CatalogAction): ConnectionState {
      98            1 :     if (action.type === RELATIONS_TOUCHED) {
      99            1 :         return state;
     100              :     }
     101            1 :     if (action.type == UPDATE_CATALOG) {
     102            1 :         const [updateId, update] = action.value;
     103            1 :         state.catalogUpdates.tasksRunning.set(updateId, update);
     104            1 :         return {
     105            1 :             ...state,
     106            1 :             catalogUpdates: {
     107            1 :                 ...state.catalogUpdates,
     108            1 :                 currentFullRefresh: update.taskVariant == CatalogUpdateVariant.FULL_CATALOG_REFRESH
     109            1 :                     ? updateId
     110            1 :                     : state.catalogUpdates.currentFullRefresh,
     111              :                 // An update takes the relations it answers. A statement may have touched others meanwhile, and a full
     112              :                 // refresh leaves them all: only the answer about a relation can say that it is gone.
     113            1 :                 touchedRelations: state.catalogUpdates.touchedRelations.filter(relation => !(update.touchedRelations ?? []).includes(relation)),
     114            1 :             },
     115            1 :         };
     116            1 :     }
     117              : 
     118            1 :     const updateId = action.value[0];
     119            1 :     const update = state.catalogUpdates.tasksRunning.get(updateId);
     120            1 :     if (!update) {
     121            1 :         return state;
     122              :     }
     123            1 :     if (action.type === CATALOG_UPDATE_REGISTER_QUERY) {
     124            1 :         state.catalogUpdates.tasksRunning.set(updateId, { ...update, queries: [...update.queries, action.value[1]] });
     125            1 :         return { ...state, catalogUpdates: { ...state.catalogUpdates } };
     126            1 :     }
     127              : 
     128              :     // The update is done
     129            1 :     const statusOf = {
     130            1 :         [CATALOG_UPDATE_CANCELLED]: CatalogUpdateTaskStatus.CANCELLED,
     131            1 :         [CATALOG_UPDATE_FAILED]: CatalogUpdateTaskStatus.FAILED,
     132            1 :         [CATALOG_UPDATE_SUCCEEDED]: CatalogUpdateTaskStatus.SUCCEEDED,
     133            1 :         [CATALOG_UPDATE_PARTIALLY_SUCCEEDED]: CatalogUpdateTaskStatus.PARTIALLY_SUCCEEDED,
     134            1 :     };
     135            1 :     const succeeded = action.type === CATALOG_UPDATE_SUCCEEDED;
     136            1 :     state.catalogUpdates.tasksRunning.delete(updateId);
     137            1 :     state.catalogUpdates.tasksFinished.set(updateId, {
     138            1 :         ...update,
     139            1 :         status: statusOf[action.type],
     140            1 :         error: succeeded ? null : action.value[1],
     141            1 :         finishedAt: new Date(),
     142            1 :     });
     143            1 :     return {
     144            1 :         ...state,
     145            1 :         catalogUpdates: {
     146            1 :             ...state.catalogUpdates,
     147            1 :             described: state.catalogUpdates.described || (succeeded && update.taskVariant === CatalogUpdateVariant.FULL_CATALOG_REFRESH),
     148              :             // A cancelled update was superseded or its connection stopped, that is no failure of the connection. A database
     149              :             // that failed in a partial update didn't answer about its relations.
     150            1 :             ...(succeeded ? { touchedFailures: 0 } : returnTouched(state, update, action.type !== CATALOG_UPDATE_CANCELLED)),
     151            1 :         },
     152            1 :     };
     153            1 : }
        

Generated by: LCOV version 2.0-1