LCOV - code coverage report
Current view: top level - dashql-app/src/compute - computation_state.ts Coverage Total Hit
Test: DashQL 915828b Lines: 91.9 % 1020 937
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : 
       3              : import { OrderByConstraint } from './sql/sqlframe_builder.js';
       4              : import { ColumnAggregationVariant, TableAggregationTask, TableOrderingTask, TableAggregation, TaskProgress, ColumnGroup, SystemColumnComputationTask, FilterTable, ROWNUMBER_COLUMN, ORDINAL_COLUMN, STRING_COLUMN, LIST_COLUMN, SKIPPED_COLUMN, ColumnAggregationTask, OrderingTable, TableFilteringTask, WithProgress, TaskStatus, WithFilter, WithFilterEpoch, ComputationStateVersion, ResultSearchState, createResultSearchState, DataSearchTable, DataSearchTask } from './computation_types.js';
       5              : import { VariantKind } from '../utils/variant.js';
       6              : import { CrossFilters } from './cross_filters.js';
       7              : import { DataFrame, DataFrameRegistry } from './data_frame.js';
       8              : import type { EmbeddedClientContext } from '../platform/database/embedded_database.js';
       9              : import { COLUMN_AGGREGATION_TASK, DATA_SEARCH_TASK, FILTERED_COLUMN_AGGREGATION_TASK, SYSTEM_COLUMN_COMPUTATION_TASK, TABLE_AGGREGATION_TASK, TABLE_FILTERING_TASK, TABLE_ORDERING_TASK, TaskVariant } from './computation_types.js';
      10              : import { parseDataSearchMatches } from './sql/query_result_search_sql.js';
      11              : 
      12              : const LOG_CTX = 'computation_state';
      13              : 
      14              : /// The table computation state
      15              : export interface TableComputationState {
      16              :     /// The table id
      17              :     tableId: number;
      18              :     /// The connection that ran the query of the table, its query history lists the computation queries.
      19              :     /// Null for a host without connections.
      20              :     connectionId: string | null;
      21              :     /// The client context of the queries of a computation without a connection, e.g. of the connection overview
      22              :     clientContext: EmbeddedClientContext | null;
      23              :     /// The tasks
      24              :     tasks: TableComputationTasks;
      25              : 
      26              :     /// The current version tracking data, filter, and ordering changes
      27              :     version: ComputationStateVersion;
      28              :     /// The table on the main thread
      29              :     dataTable: arrow.Table;
      30              :     /// The table field index
      31              :     dataTableFieldsByName: Map<string, number>;
      32              :     /// The abort controller
      33              :     dataTableLifetime: AbortController;
      34              :     /// The data frame in the compute module
      35              :     dataFrame: DataFrame | null;
      36              :     /// The grid columns
      37              :     columnGroups: ColumnGroup[];
      38              :     /// The ordering constraints
      39              :     dataTableOrdering: OrderByConstraint[];
      40              : 
      41              :     /// The active cross-filter selection.
      42              :     /// This is the source of truth for cross-filtering and lives here (rather than
      43              :     /// as component-local state) so that every view of the same table - the result
      44              :     /// table header plots and the visualization tab's aggregation bar - shares one
      45              :     /// selection and drives a single `filterTable`.
      46              :     crossFilters: CrossFilters;
      47              :     /// The active filter table (if any)
      48              :     filterTable: FilterTable | null;
      49              :     /// The active ordering table (if any)
      50              :     orderingTable: OrderingTable | null;
      51              :     /// Shared Columns search for every view of this result
      52              :     columnSearch: ResultSearchState;
      53              :     /// Shared Data search for every view of this result
      54              :     dataSearch: ResultSearchState;
      55              :     /// The materialized rows and matching columns for the applied Data search
      56              :     dataSearchTable: DataSearchTable | null;
      57              :     /// The row number column group
      58              :     rowNumberColumnGroup: number | null;
      59              :     /// The row number column name
      60              :     rowNumberColumnName: string | null;
      61              :     /// The table aggregation
      62              :     tableAggregation: TableAggregation | null;
      63              :     /// The column (group) aggregates
      64              :     columnAggregates: (ColumnAggregationVariant | null)[];
      65              :     /// The filtered column (group) aggregates
      66              :     filteredColumnAggregates: (WithFilterEpoch<ColumnAggregationVariant> | null)[];
      67              :     /// Indicates which filtered column aggregates are outdated for the current filter
      68              :     filteredColumnAggregatesOutdated: boolean[];
      69              : }
      70              : 
      71              : export interface TableComputationTasks {
      72              :     /// The table aggregation task
      73              :     tableAggregationTask: WithProgress<TableAggregationTask> | null;
      74              :     /// The task to precompute system columns
      75              :     systemColumnTask: WithProgress<SystemColumnComputationTask> | null;
      76              :     /// The filtering task
      77              :     filteringTask: WithProgress<TableFilteringTask> | null;
      78              :     /// The ordering task
      79              :     orderingTask: WithProgress<TableOrderingTask> | null;
      80              :     /// The Data-search materialization task
      81              :     dataSearchTask: WithProgress<DataSearchTask> | null;
      82              :     /// The task to compute column (group) aggregates.
      83              :     /// The array contains N entries where N is the number of column groups
      84              :     columnAggregationTasks: (WithProgress<ColumnAggregationTask> | null)[];
      85              :     /// The task to compute filtered column (group) aggregates
      86              :     filteredColumnAggregationTasks: (WithProgress<WithFilter<ColumnAggregationTask>> | null)[];
      87              : }
      88              : 
      89              : /// The computation registry.
      90              : /// We store the scheduler tasks here as well to allow for linking the "latest" tasks safely in the per-table computation states.
      91              : export interface ComputationState {
      92              :     /// The computations
      93              :     tableComputations: { [key: number]: TableComputationState };
      94              : 
      95              :     /// The scheduler tasks
      96              :     schedulerTasks: { [key: number]: TaskVariant };
      97              :     /// The next task id
      98              :     nextSchedulerTaskId: number;
      99              : }
     100              : 
     101              : /// Create the computation state
     102            1 : export function createComputationState(): ComputationState {
     103            1 :     return {
     104            1 :         tableComputations: {},
     105            1 :         schedulerTasks: {},
     106            1 :         nextSchedulerTaskId: 1,
     107            1 :     };
     108            1 : }
     109              : 
     110            1 : export function createArrowFieldIndex(table: arrow.Table): Map<string, number> {
     111            1 :     let out = new Map<string, number>();
     112            1 :     for (let i = 0; i < table.schema.fields.length; ++i) {
     113            1 :         out.set(table.schema.fields[i].name, i);
     114              :     }
     115            1 :     return out;
     116            1 : }
     117              : 
     118              : /// Create the table computation state
     119            1 : export function createTableComputationState(computationId: number, table: arrow.Table, tableColumns: ColumnGroup[], tableLifetime: AbortController, connectionId: string | null = null, clientContext: EmbeddedClientContext | null = null): TableComputationState {
     120            1 :     return {
     121            1 :         tableId: computationId,
     122            1 :         connectionId,
     123            1 :         clientContext,
     124            1 :         tasks: {
     125            1 :             filteringTask: null,
     126            1 :             orderingTask: null,
     127            1 :             dataSearchTask: null,
     128            1 :             tableAggregationTask: null,
     129            1 :             systemColumnTask: null,
     130            1 :             columnAggregationTasks: Array(tableColumns.length + 1).fill(null),
     131            1 :             filteredColumnAggregationTasks: Array(tableColumns.length + 1).fill(null),
     132            1 :         },
     133            1 :         dataTable: table,
     134            1 :         version: new ComputationStateVersion(0, 0),
     135            1 :         dataTableFieldsByName: createArrowFieldIndex(table),
     136            1 :         columnGroups: tableColumns,
     137            1 :         columnAggregates: Array.from({ length: tableColumns.length }, () => null),
     138            1 :         rowNumberColumnGroup: null,
     139            1 :         rowNumberColumnName: null,
     140            1 :         dataTableLifetime: tableLifetime,
     141            1 :         dataTableOrdering: [],
     142            1 :         crossFilters: new CrossFilters(),
     143            1 :         dataFrame: null,
     144            1 :         filterTable: null,
     145            1 :         orderingTable: null,
     146            1 :         columnSearch: createResultSearchState(),
     147            1 :         dataSearch: createResultSearchState(),
     148            1 :         dataSearchTable: null,
     149            1 :         filteredColumnAggregates: Array.from({ length: tableColumns.length }, () => null),
     150            1 :         filteredColumnAggregatesOutdated: Array.from({ length: tableColumns.length }, () => false),
     151            1 :         tableAggregation: null,
     152            1 :     };
     153            1 : }
     154              : 
     155            1 : export const COMPUTATION_FROM_QUERY_RESULT = Symbol('COMPUTATION_FROM_QUERY_RESULT');
     156            1 : export const DELETE_COMPUTATION = Symbol('DELETE_COMPUTATION');
     157            1 : export const CREATED_DATA_FRAME = Symbol('CREATED_DATA_FRAME');
     158            1 : export const SCHEDULE_TASK = Symbol('SCHEDULE_TASK');
     159            1 : export const UPDATE_SCHEDULER_TASK = Symbol('UPDATE_SCHEDULER_TASK');
     160            1 : export const UNREGISTER_SCHEDULER_TASK = Symbol('UNREGISTER_SCHEDULER_TASK');
     161            1 : export const TABLE_ORDERING_SUCCEDED = Symbol('TABLE_ORDERING_SUCCEDED');
     162            1 : export const CLEAR_TABLE_ORDERING = Symbol('CLEAR_TABLE_ORDERING');
     163            1 : export const TABLE_FILTERING_SUCCEEDED = Symbol('TABLE_FILTERING_SUCCEEDED');
     164            1 : export const TABLE_AGGREGATION_SUCCEEDED = Symbol('TABLE_AGGREGATION_SUCCEEDED');
     165            1 : export const SYSTEM_COLUMN_COMPUTATION_SUCCEEDED = Symbol('SYSTEM_COLUMN_COMPUTATION_SUCCEEDED');
     166            1 : export const COLUMN_AGGREGATION_SUCCEEDED = Symbol('COLUMN_AGGREGATION_SUCCEEDED');
     167            1 : export const FILTERED_COLUMN_AGGREGATION_SUCCEEDED = Symbol('FILTERED_COLUMN_AGGREGATION_SUCCEEDED');
     168            1 : export const SET_CROSS_FILTERS = Symbol('SET_CROSS_FILTERS');
     169            1 : export const SET_RESULT_SEARCH_PATTERN = Symbol('SET_RESULT_SEARCH_PATTERN');
     170            1 : export const COLUMN_SEARCH_SUCCEEDED = Symbol('COLUMN_SEARCH_SUCCEEDED');
     171            1 : export const DATA_SEARCH_SUCCEEDED = Symbol('DATA_SEARCH_SUCCEEDED');
     172            1 : export const DATA_SEARCH_FAILED = Symbol('DATA_SEARCH_FAILED');
     173            1 : export const RESULT_SEARCH_FAILED = Symbol('RESULT_SEARCH_FAILED');
     174              : 
     175              : export type ComputationAction =
     176              :     | VariantKind<typeof SCHEDULE_TASK, TaskVariant>
     177              :     | VariantKind<typeof UPDATE_SCHEDULER_TASK, [TaskVariant, Partial<TaskProgress>]>
     178              :     | VariantKind<typeof UNREGISTER_SCHEDULER_TASK, TaskVariant>
     179              : 
     180              :     | VariantKind<typeof COMPUTATION_FROM_QUERY_RESULT, [number, arrow.Table, ColumnGroup[], AbortController, (string | null)?, (EmbeddedClientContext | null)?]>
     181              :     | VariantKind<typeof DELETE_COMPUTATION, [number]>
     182              :     | VariantKind<typeof CREATED_DATA_FRAME, [number, DataFrame]>
     183              : 
     184              :     | VariantKind<typeof TABLE_ORDERING_SUCCEDED, [number, OrderingTable]>
     185              :     | VariantKind<typeof CLEAR_TABLE_ORDERING, number>
     186              :     | VariantKind<typeof TABLE_FILTERING_SUCCEEDED, [number, ComputationStateVersion, FilterTable | null]>
     187              :     | VariantKind<typeof TABLE_AGGREGATION_SUCCEEDED, [number, TableAggregation]>
     188              :     | VariantKind<typeof SYSTEM_COLUMN_COMPUTATION_SUCCEEDED, [number, arrow.Table, DataFrame, ColumnGroup[]]>
     189              :     | VariantKind<typeof COLUMN_AGGREGATION_SUCCEEDED, [number, number, ColumnAggregationVariant]>
     190              :     | VariantKind<typeof FILTERED_COLUMN_AGGREGATION_SUCCEEDED, [number, number, ComputationStateVersion, WithFilterEpoch<ColumnAggregationVariant> | null]>
     191              :     | VariantKind<typeof SET_CROSS_FILTERS, [number, CrossFilters]>
     192              :     | VariantKind<typeof SET_RESULT_SEARCH_PATTERN, [number, 'columns' | 'data', string]>
     193              :     | VariantKind<typeof COLUMN_SEARCH_SUCCEEDED, [number, DataFrame, number, number[]]>
     194              :     | VariantKind<typeof DATA_SEARCH_SUCCEEDED, [number, DataFrame, number, DataSearchTable]>
     195              :     | VariantKind<typeof DATA_SEARCH_FAILED, [number, DataFrame, number, string]>
     196              :     | VariantKind<typeof RESULT_SEARCH_FAILED, [number, DataFrame, 'columns' | 'data', number, string]>
     197              :     ;
     198              : 
     199            1 : export function reduceComputationState(state: ComputationState, action: ComputationAction, memory: DataFrameRegistry): ComputationState {
     200            1 :     switch (action.type) {
     201            1 :         case SCHEDULE_TASK: {
     202            1 :             const taskVariant = action.value;
     203              :             const initialProgress: TaskProgress = {
     204            1 :                 status: TaskStatus.TASK_RUNNING,
     205            1 :                 startedAt: new Date(),
     206            1 :                 completedAt: null,
     207            1 :                 failedAt: null,
     208            1 :                 failedWithError: null,
     209              :             };
     210            1 :             return updateTask(state, taskVariant, initialProgress, memory, true);
     211            1 :         }
     212            1 :         case UPDATE_SCHEDULER_TASK: {
     213            1 :             const [taskVariant, progress] = action.value;
     214            1 :             return updateTask(state, taskVariant, progress, memory, false);
     215            1 :         }
     216            1 :         case UNREGISTER_SCHEDULER_TASK: {
     217            1 :             if (action.value.taskId === undefined) {
     218            0 :                 return state;
     219              :             }
     220            1 :             if (state.schedulerTasks[action.value.taskId] !== action.value) {
     221            0 :                 return state;
     222              :             }
     223            1 :             const schedulerTasks = { ...state.schedulerTasks };
     224            1 :             delete schedulerTasks[action.value.taskId];
     225            1 :             releaseTaskDataFrames(action.value, memory);
     226            1 :             return {
     227            1 :                 ...state,
     228            1 :                 schedulerTasks
     229            1 :             };
     230            1 :         }
     231              : 
     232            1 :         case COMPUTATION_FROM_QUERY_RESULT: {
     233            1 :             const [tableId, table, tableColumns, tableLifetime, connectionId, clientContext] = action.value;
     234            1 :             const previousTableState = state.tableComputations[tableId];
     235            1 :             if (previousTableState !== undefined) {
     236            1 :                 destroyTableComputationState(previousTableState, memory);
     237              :             }
     238            1 :             const tableState = createTableComputationState(tableId, table, tableColumns, tableLifetime, connectionId ?? null, clientContext ?? null);
     239            1 :             return {
     240            1 :                 ...state,
     241            1 :                 tableComputations: {
     242            1 :                     ...state.tableComputations,
     243            1 :                     [tableId]: tableState
     244            1 :                 }
     245            1 :             };
     246            1 :         }
     247            1 :         case DELETE_COMPUTATION: {
     248            1 :             const [tableId] = action.value;
     249            1 :             const tableState = state.tableComputations[tableId];
     250            1 :             if (tableState === undefined) {
     251            1 :                 return state;
     252              :             }
     253            1 :             const tableComputations = { ...state.tableComputations };
     254            1 :             destroyTableComputationState(tableState, memory);
     255            1 :             delete tableComputations[tableId];
     256            1 :             return {
     257            1 :                 ...state,
     258            1 :                 tableComputations
     259            1 :             };
     260            1 :         }
     261            1 :         case CREATED_DATA_FRAME: {
     262            1 :             const [tableId, dataFrame] = action.value;
     263            1 :             memory.acquire(dataFrame);
     264              : 
     265            1 :             const prevTableState = state.tableComputations[tableId]!;
     266            1 :             const nextTableState: TableComputationState = {
     267            1 :                 ...prevTableState,
     268            1 :                 version: prevTableState.version.withDataIncrement(),
     269            1 :                 dataFrame,
     270            1 :             };
     271            1 :             memory.release(prevTableState.dataFrame);
     272            1 :             return {
     273            1 :                 ...state,
     274            1 :                 tableComputations: {
     275            1 :                     ...state.tableComputations,
     276            1 :                     [tableId]: nextTableState
     277            1 :                 }
     278            1 :             };
     279            1 :         }
     280            1 :         case TABLE_ORDERING_SUCCEDED: {
     281            1 :             const [tableId, orderingTable] = action.value;
     282            1 :             memory.acquire(orderingTable.dataFrame);
     283              : 
     284            1 :             const tableState = state.tableComputations[tableId];
     285            1 :             if (tableState === undefined) {
     286            0 :                 memory.release(orderingTable.dataFrame);
     287            0 :                 return state;
     288            0 :             }
     289              :             // Ordering depends on both data and filter, so check both
     290            1 :             if (!orderingTable.version.filterMatches(tableState.version)) {
     291            1 :                 memory.release(orderingTable.dataFrame);
     292            1 :                 return state;
     293            1 :             }
     294            1 :             if ((tableState.dataSearchTable?.requestId ?? null) !== (orderingTable.dataSearchRequestId ?? null)) {
     295            0 :                 memory.release(orderingTable.dataFrame);
     296            0 :                 return state;
     297            0 :             }
     298            1 :             const requestedOrdering = tableState.tasks.orderingTask?.orderingConstraints;
     299            1 :             if (requestedOrdering == null || !areOrderingConstraintsEqual(requestedOrdering, orderingTable.orderingConstraints)) {
     300            1 :                 memory.release(orderingTable.dataFrame);
     301            1 :                 return state;
     302            1 :             }
     303              : 
     304            1 :             const task = !tableState.tasks.orderingTask ? null : {
     305            1 :                 ...tableState.tasks.orderingTask,
     306            1 :                 progress: {
     307            1 :                     ...tableState.tasks.orderingTask.progress,
     308            1 :                     status: TaskStatus.TASK_SUCCEEDED,
     309            1 :                     completedAt: new Date(),
     310            1 :                 }
     311            1 :             };
     312            1 :             memory.release(tableState.orderingTable?.dataFrame);
     313            1 :             return {
     314            1 :                 ...state,
     315            1 :                 tableComputations: {
     316            1 :                     ...state.tableComputations,
     317            1 :                     [tableId]: {
     318            1 :                         ...tableState,
     319            1 :                         orderingTable,
     320            1 :                         dataTableOrdering: orderingTable.orderingConstraints,
     321            1 :                         tasks: {
     322            1 :                             ...tableState.tasks,
     323            1 :                             orderingTask: task
     324            1 :                         }
     325            1 :                     }
     326            1 :                 }
     327            1 :             };
     328            1 :         }
     329            1 :         case CLEAR_TABLE_ORDERING: {
     330            1 :             const tableState = state.tableComputations[action.value];
     331            1 :             if (tableState === undefined) {
     332            0 :                 return state;
     333              :             }
     334            1 :             memory.release(tableState.orderingTable?.dataFrame);
     335            1 :             return {
     336            1 :                 ...state,
     337            1 :                 tableComputations: {
     338            1 :                     ...state.tableComputations,
     339            1 :                     [action.value]: {
     340            1 :                         ...tableState,
     341            1 :                         dataTableOrdering: [],
     342            1 :                         orderingTable: null,
     343            1 :                         tasks: {
     344            1 :                             ...tableState.tasks,
     345            1 :                             orderingTask: tableState.tasks.orderingTask == null ? null : {
     346            0 :                                 ...tableState.tasks.orderingTask,
     347            0 :                                 orderingConstraints: [],
     348            0 :                             },
     349            1 :                         },
     350            1 :                     },
     351            1 :                 },
     352            1 :             };
     353            1 :         }
     354            1 :         case TABLE_FILTERING_SUCCEEDED: {
     355            1 :             const [tableId, completedVersion, filterTable] = action.value;
     356            1 :             memory.acquire(filterTable?.dataFrame);
     357            1 :             const tableState = state.tableComputations[tableId];
     358            1 :             if (tableState === undefined) {
     359            0 :                 memory.release(filterTable?.dataFrame);
     360            0 :                 return state;
     361            0 :             }
     362              :             // Reject stale filter results (must match current state exactly)
     363              :             // With pre-allocated versions, only accept results that match the current target
     364            1 :             if (!completedVersion.filterMatches(tableState.version)) {
     365            1 :                 memory.release(filterTable?.dataFrame);
     366            1 :                 return state;
     367            1 :             }
     368            1 :             return tableFilteringSucceded(state, tableId, filterTable, memory);
     369            1 :         }
     370            1 :         case TABLE_AGGREGATION_SUCCEEDED: {
     371            1 :             const [tableId, tableAggregation] = action.value;
     372            1 :             memory.acquire(tableAggregation.dataFrame);
     373              : 
     374            1 :             const tableState = state.tableComputations[tableId];
     375            1 :             if (tableState === undefined) {
     376            0 :                 memory.release(tableAggregation.dataFrame);
     377            0 :                 return state;
     378            0 :             }
     379            1 :             memory.release(tableState.tableAggregation?.dataFrame);
     380              : 
     381            1 :             const task = !tableState.tasks.tableAggregationTask ? null : {
     382            1 :                 ...tableState.tasks.tableAggregationTask,
     383            1 :                 progress: {
     384            1 :                     ...tableState.tasks.tableAggregationTask.progress,
     385            1 :                     status: TaskStatus.TASK_SUCCEEDED,
     386            1 :                     completedAt: new Date(),
     387            1 :                 }
     388            1 :             };
     389            1 :             return {
     390            1 :                 ...state,
     391            1 :                 tableComputations: {
     392            1 :                     ...state.tableComputations,
     393            1 :                     [tableId]: {
     394            1 :                         ...tableState,
     395            1 :                         tableAggregation: tableAggregation,
     396            1 :                         tasks: {
     397            1 :                             ...tableState.tasks,
     398            1 :                             tableAggregationTask: task,
     399            1 :                         }
     400            1 :                     }
     401            1 :                 }
     402            1 :             };
     403            1 :         }
     404            1 :         case SYSTEM_COLUMN_COMPUTATION_SUCCEEDED: {
     405            1 :             const [tableId, dataTable, dataFrame, columnGroups] = action.value;
     406            1 :             memory.acquire(dataFrame);
     407              : 
     408            1 :             let rowNumberColumnGroup: number | null = null;
     409            1 :             let rowNumberColumnName: string | null = null;
     410            1 :             for (let i = 0; i < columnGroups.length; ++i) {
     411            1 :                 const group = columnGroups[i];
     412            1 :                 switch (group.type) {
     413            1 :                     case ROWNUMBER_COLUMN:
     414            1 :                         rowNumberColumnGroup = i;
     415            1 :                         rowNumberColumnName = group.value.rowNumberFieldName;
     416              :                         break;
     417            1 :                 }
     418            1 :             }
     419            1 :             const tableState = state.tableComputations[tableId];
     420            1 :             if (tableState === undefined) {
     421            0 :                 memory.release(dataFrame);
     422            0 :                 return state;
     423            0 :             }
     424            1 :             memory.release(tableState.dataFrame);
     425              : 
     426            1 :             const task = !tableState.tasks.systemColumnTask ? null : {
     427            1 :                 ...tableState.tasks.systemColumnTask,
     428            1 :                 progress: {
     429            1 :                     ...tableState.tasks.systemColumnTask.progress,
     430            1 :                     status: TaskStatus.TASK_SUCCEEDED,
     431            1 :                     completedAt: new Date(),
     432            1 :                 }
     433            1 :             };
     434            1 :             return {
     435            1 :                 ...state,
     436            1 :                 tableComputations: {
     437            1 :                     ...state.tableComputations,
     438            1 :                     [tableId]: {
     439            1 :                         ...tableState,
     440            1 :                         version: tableState.version.withDataIncrement(),
     441            1 :                         dataTable,
     442            1 :                         dataFrame,
     443            1 :                         columnGroups,
     444            1 :                         rowNumberColumnGroup,
     445            1 :                         rowNumberColumnName,
     446            1 :                         tasks: {
     447            1 :                             ...tableState.tasks,
     448            1 :                             systemColumnTask: task,
     449            1 :                         }
     450            1 :                     }
     451            1 :                 }
     452            1 :             };
     453            1 :         }
     454            1 :         case COLUMN_AGGREGATION_SUCCEEDED: {
     455            1 :             const [tableId, columnId, columnAggregate] = action.value;
     456            1 :             acquireColumnAggregate(columnAggregate, memory);
     457              : 
     458            1 :             const tableState = state.tableComputations[tableId];
     459            1 :             if (tableState === undefined) {
     460            1 :                 releaseColumnAggregate(columnAggregate, memory);
     461            1 :                 return state;
     462            1 :             }
     463              :             // Column aggregation unknown?
     464            1 :             const currentTask = tableState.tasks.columnAggregationTasks[columnId];
     465            1 :             if (currentTask == null) {
     466            1 :                 releaseColumnAggregate(columnAggregate, memory);
     467            1 :                 return state;
     468            1 :             }
     469              :             // Destroy the previous column aggregate, if there is one
     470            1 :             const prevAggregate = tableState.columnAggregates[columnId];
     471            1 :             if (prevAggregate != null) {
     472            1 :                 releaseColumnAggregate(prevAggregate, memory);
     473              :             }
     474            1 :             const columnAggregates: (ColumnAggregationVariant | null)[] = [...tableState.columnAggregates];
     475            1 :             const columnAggregationTasks = [...tableState.tasks.columnAggregationTasks];
     476            1 :             columnAggregates[columnId] = columnAggregate;
     477              :             const aggregationTask: WithProgress<ColumnAggregationTask> = {
     478            1 :                 ...currentTask,
     479            1 :                 progress: {
     480            1 :                     ...currentTask.progress,
     481            1 :                     completedAt: new Date(),
     482            1 :                     status: TaskStatus.TASK_SUCCEEDED,
     483            1 :                 }
     484              :             };
     485            1 :             columnAggregationTasks[columnId] = aggregationTask;
     486              : 
     487            1 :             return {
     488            1 :                 ...state,
     489            1 :                 tableComputations: {
     490            1 :                     ...state.tableComputations,
     491            1 :                     [tableId]: {
     492            1 :                         ...tableState,
     493            1 :                         columnAggregates,
     494            1 :                         tasks: {
     495            1 :                             ...tableState.tasks,
     496            1 :                             columnAggregationTasks
     497            1 :                         }
     498            1 :                     }
     499            1 :                 }
     500            1 :             };
     501            1 :         }
     502            1 :         case FILTERED_COLUMN_AGGREGATION_SUCCEEDED: {
     503            1 :             const [tableId, columnId, completedFilterVersion, newColumnAggregate] = action.value;
     504            1 :             acquireColumnAggregate(newColumnAggregate, memory);
     505              : 
     506              :             // Computation not registered?
     507            1 :             const tableState = state.tableComputations[tableId];
     508            1 :             if (tableState === undefined) {
     509            0 :                 releaseColumnAggregate(newColumnAggregate, memory);
     510            0 :                 return state;
     511            0 :             }
     512              :             // Column aggregation task unknown?
     513            1 :             const currentTask = tableState.tasks.filteredColumnAggregationTasks[columnId];
     514            1 :             if (currentTask == null) {
     515            0 :                 releaseColumnAggregate(newColumnAggregate, memory);
     516            0 :                 return state;
     517            0 :             }
     518            1 :             if (!currentTask.tableVersion.filterMatches(completedFilterVersion)) {
     519            1 :                 releaseColumnAggregate(newColumnAggregate, memory);
     520            1 :                 return state;
     521            1 :             }
     522            1 :             const currentSelectionVersion = tableState.filterTable?.version ?? tableState.version;
     523            1 :             if (!currentSelectionVersion.filterMatches(completedFilterVersion)) {
     524            1 :                 releaseColumnAggregate(newColumnAggregate, memory);
     525            1 :                 return state;
     526            1 :             }
     527            1 :             if ((newColumnAggregate?.dataSearchRequestId ?? null) !== (tableState.dataSearchTable?.requestId ?? null)) {
     528            0 :                 releaseColumnAggregate(newColumnAggregate, memory);
     529            0 :                 return state;
     530            0 :             }
     531            1 :             if ((currentTask.dataSearchTable?.requestId ?? null) !== (tableState.dataSearchTable?.requestId ?? null)) {
     532            0 :                 releaseColumnAggregate(newColumnAggregate, memory);
     533            0 :                 return state;
     534            0 :             }
     535              : 
     536            1 :             const filteredColumnAggregates: (WithFilterEpoch<ColumnAggregationVariant> | null)[] = [...tableState.filteredColumnAggregates];
     537            1 :             const filteredColumnAggregatesOutdated = [...tableState.filteredColumnAggregatesOutdated];
     538            1 :             const filteredColumnAggregationTasks = [...tableState.tasks.filteredColumnAggregationTasks];
     539            1 :             const prevColumnAggregate = filteredColumnAggregates[columnId];
     540            1 :             filteredColumnAggregates[columnId] = newColumnAggregate;
     541              :             const task: WithProgress<WithFilter<ColumnAggregationTask>> = {
     542            1 :                 ...currentTask,
     543            1 :                 progress: {
     544            1 :                     ...currentTask.progress,
     545            1 :                     completedAt: new Date(),
     546            1 :                     status: TaskStatus.TASK_SUCCEEDED,
     547            1 :                 }
     548              :             };
     549            1 :             filteredColumnAggregationTasks[columnId] = task;
     550              : 
     551            1 :             const aggregateUpToDate = newColumnAggregate != null
     552            1 :                 && newColumnAggregate.filterVersion.filterMatches(currentSelectionVersion)
     553            1 :                 && newColumnAggregate.dataSearchRequestId === (tableState.dataSearchTable?.requestId ?? null);
     554            1 :             filteredColumnAggregatesOutdated[columnId] = !aggregateUpToDate;
     555              : 
     556              :             // Destroy the previous column aggregate, if there is one
     557            1 :             if (prevColumnAggregate != null) {
     558            0 :                 releaseColumnAggregate(prevColumnAggregate, memory);
     559              :             }
     560            1 :             return {
     561            1 :                 ...state,
     562            1 :                 tableComputations: {
     563            1 :                     ...state.tableComputations,
     564            1 :                     [tableId]: {
     565            1 :                         ...tableState,
     566            1 :                         filteredColumnAggregates,
     567            1 :                         filteredColumnAggregatesOutdated,
     568            1 :                         tasks: {
     569            1 :                             ...tableState.tasks,
     570            1 :                             filteredColumnAggregationTasks
     571            1 :                         }
     572            1 :                     }
     573            1 :                 },
     574            1 :             };
     575            1 :         }
     576            1 :         case SET_CROSS_FILTERS: {
     577            1 :             const [tableId, crossFilters] = action.value;
     578            1 :             const tableState = state.tableComputations[tableId];
     579            1 :             if (tableState === undefined) {
     580            1 :                 return state;
     581              :             }
     582              :             // No change? Keep the same state to avoid a spurious re-render / filtering pass.
     583            1 :             if (tableState.crossFilters.equals(crossFilters)) {
     584            1 :                 return state;
     585              :             }
     586            1 :             return {
     587            1 :                 ...state,
     588            1 :                 tableComputations: {
     589            1 :                     ...state.tableComputations,
     590            1 :                     [tableId]: {
     591            1 :                         ...tableState,
     592            1 :                         crossFilters,
     593            1 :                     }
     594            1 :                 }
     595            1 :             };
     596            1 :         }
     597            1 :         case SET_RESULT_SEARCH_PATTERN: {
     598            1 :             const [tableId, kind, pattern] = action.value;
     599            1 :             const tableState = state.tableComputations[tableId];
     600            1 :             if (tableState === undefined) {
     601            0 :                 return state;
     602              :             }
     603            1 :             const current = kind === 'columns' ? tableState.columnSearch : tableState.dataSearch;
     604            1 :             if (current.requestedPattern === pattern && current.pending === (pattern.length > 0)) {
     605            0 :                 return state;
     606              :             }
     607            1 :             const nextSearch: ResultSearchState = pattern.length === 0
     608            0 :                 ? {
     609            0 :                     ...createResultSearchState(),
     610            0 :                     requestId: current.requestId + 1,
     611            0 :                     appliedRequestId: current.requestId + 1,
     612            0 :                 }
     613            1 :                 : {
     614            1 :                     ...current,
     615            1 :                     requestedPattern: pattern,
     616            1 :                     requestId: current.requestId + 1,
     617            1 :                     pending: true,
     618            1 :                     error: null,
     619            1 :                 };
     620            1 :             if (kind === 'data') {
     621            1 :                 tableState.tasks.dataSearchTask?.abortController.abort();
     622              :             }
     623            1 :             const clearAppliedDataSearch = kind === 'data' && pattern.length === 0;
     624            1 :             if (clearAppliedDataSearch) {
     625            0 :                 memory.release(tableState.dataSearchTable?.dataFrame);
     626            0 :                 memory.release(tableState.orderingTable?.dataFrame);
     627            0 :                 memory.release(tableState.tasks.dataSearchTask?.inputDataFrame);
     628            0 :                 if (tableState.filterTable == null) {
     629            0 :                     releaseColumnAggregates(tableState.filteredColumnAggregates, memory);
     630              :                 }
     631            0 :             }
     632            1 :             const stateForOutdatedCheck = clearAppliedDataSearch
     633            0 :                 ? { ...tableState, dataSearchTable: null }
     634            1 :                 : tableState;
     635            1 :             return {
     636            1 :                 ...state,
     637            1 :                 tableComputations: {
     638            1 :                     ...state.tableComputations,
     639            1 :                     [tableId]: {
     640            1 :                         ...tableState,
     641            1 :                         columnSearch: kind === 'columns' ? nextSearch : tableState.columnSearch,
     642            1 :                         dataSearch: kind === 'data' ? nextSearch : tableState.dataSearch,
     643            1 :                         dataSearchTable: clearAppliedDataSearch ? null : tableState.dataSearchTable,
     644            1 :                         orderingTable: clearAppliedDataSearch ? null : tableState.orderingTable,
     645            1 :                         tasks: clearAppliedDataSearch
     646            0 :                             ? { ...tableState.tasks, dataSearchTask: null }
     647            1 :                             : tableState.tasks,
     648            1 :                         filteredColumnAggregatesOutdated: clearAppliedDataSearch
     649            0 :                             ? getFilteredColumnAggregatesOutdated(stateForOutdatedCheck, null)
     650            1 :                             : tableState.filteredColumnAggregatesOutdated,
     651            1 :                         filteredColumnAggregates: clearAppliedDataSearch && tableState.filterTable == null
     652            0 :                             ? Array.from({ length: tableState.filteredColumnAggregates.length }, () => null)
     653            1 :                             : tableState.filteredColumnAggregates,
     654            1 :                     }
     655            1 :                 }
     656            1 :             };
     657            1 :         }
     658            1 :         case COLUMN_SEARCH_SUCCEEDED: {
     659            1 :             const [tableId, sourceDataFrame, requestId, matchingColumnGroups] = action.value;
     660            1 :             const tableState = state.tableComputations[tableId];
     661            1 :             if (tableState === undefined || tableState.dataFrame !== sourceDataFrame || tableState.columnSearch.requestId !== requestId) {
     662            0 :                 return state;
     663              :             }
     664            1 :             return {
     665            1 :                 ...state,
     666            1 :                 tableComputations: {
     667            1 :                     ...state.tableComputations,
     668            1 :                     [tableId]: {
     669            1 :                         ...tableState,
     670            1 :                         columnSearch: {
     671            1 :                             ...tableState.columnSearch,
     672            1 :                             appliedPattern: tableState.columnSearch.requestedPattern,
     673            1 :                             appliedRequestId: requestId,
     674            1 :                             matchingColumnGroups,
     675            1 :                             pending: false,
     676            1 :                             error: null,
     677            1 :                         }
     678            1 :                     }
     679            1 :                 }
     680            1 :             };
     681            1 :         }
     682            1 :         case DATA_SEARCH_SUCCEEDED: {
     683            1 :             const [tableId, sourceDataFrame, requestId, dataSearchTable] = action.value;
     684            1 :             memory.acquire(dataSearchTable.dataFrame);
     685            1 :             const tableState = state.tableComputations[tableId];
     686            1 :             if (
     687            1 :                 tableState === undefined
     688            1 :                 || tableState.dataFrame !== sourceDataFrame
     689            1 :                 || tableState.dataSearch.requestId !== requestId
     690            1 :                 || !dataSearchTable.version.dataMatches(tableState.version)
     691            0 :             ) {
     692            0 :                 memory.release(dataSearchTable.dataFrame);
     693            0 :                 return state;
     694            0 :             }
     695            1 :             const matchingRows = parseDataSearchMatches(dataSearchTable.dataTable);
     696            1 :             memory.release(tableState.dataSearchTable?.dataFrame);
     697            1 :             memory.release(tableState.orderingTable?.dataFrame);
     698            1 :             if (tableState.filterTable == null) {
     699            1 :                 releaseColumnAggregates(tableState.filteredColumnAggregates, memory);
     700              :             }
     701            1 :             const dataSearchTask = tableState.tasks.dataSearchTask == null ? null : {
     702            0 :                 ...tableState.tasks.dataSearchTask,
     703            0 :                 progress: {
     704            0 :                     ...tableState.tasks.dataSearchTask.progress,
     705            0 :                     status: TaskStatus.TASK_SUCCEEDED,
     706            0 :                     completedAt: new Date(),
     707            0 :                 },
     708            0 :             };
     709            1 :             const effectiveVersion = tableState.filterTable?.version ?? tableState.version;
     710            1 :             return {
     711            1 :                 ...state,
     712            1 :                 tableComputations: {
     713            1 :                     ...state.tableComputations,
     714            1 :                     [tableId]: {
     715            1 :                         ...tableState,
     716            1 :                         dataSearch: {
     717            1 :                             ...tableState.dataSearch,
     718            1 :                             appliedPattern: tableState.dataSearch.requestedPattern,
     719            1 :                             appliedRequestId: requestId,
     720            1 :                             matchingRows,
     721            1 :                             pending: false,
     722            1 :                             error: null,
     723            1 :                         },
     724            1 :                         dataSearchTable,
     725            1 :                         orderingTable: null,
     726            1 :                         filteredColumnAggregatesOutdated: getFilteredColumnAggregatesOutdated(
     727            1 :                             { ...tableState, version: effectiveVersion },
     728            1 :                             dataSearchTable,
     729            1 :                         ),
     730            1 :                         tasks: { ...tableState.tasks, dataSearchTask },
     731            1 :                     }
     732            1 :                 }
     733            1 :             };
     734            1 :         }
     735            1 :         case DATA_SEARCH_FAILED: {
     736            1 :             const [tableId, sourceDataFrame, requestId, error] = action.value;
     737            1 :             const tableState = state.tableComputations[tableId];
     738            1 :             if (tableState === undefined || tableState.dataFrame !== sourceDataFrame || tableState.dataSearch.requestId !== requestId) {
     739            1 :                 return state;
     740              :             }
     741            1 :             memory.release(tableState.dataSearchTable?.dataFrame);
     742            1 :             memory.release(tableState.orderingTable?.dataFrame);
     743            1 :             return {
     744            1 :                 ...state,
     745            1 :                 tableComputations: {
     746            1 :                     ...state.tableComputations,
     747            1 :                     [tableId]: {
     748            1 :                         ...tableState,
     749            1 :                         dataSearch: {
     750            1 :                             ...tableState.dataSearch,
     751            1 :                             appliedPattern: '',
     752            1 :                             appliedRequestId: requestId,
     753            1 :                             matchingRows: null,
     754            1 :                             pending: false,
     755            1 :                             error,
     756            1 :                         },
     757            1 :                         dataSearchTable: null,
     758            1 :                         orderingTable: null,
     759            1 :                         filteredColumnAggregatesOutdated: getFilteredColumnAggregatesOutdated({ ...tableState, dataSearchTable: null }, null),
     760            1 :                         filteredColumnAggregates: tableState.filterTable == null
     761            1 :                             ? Array.from({ length: tableState.filteredColumnAggregates.length }, () => null)
     762            0 :                             : tableState.filteredColumnAggregates,
     763            1 :                     },
     764            1 :                 },
     765            1 :             };
     766            1 :         }
     767            1 :         case RESULT_SEARCH_FAILED: {
     768            1 :             const [tableId, sourceDataFrame, kind, requestId, error] = action.value;
     769            1 :             const tableState = state.tableComputations[tableId];
     770            1 :             if (tableState === undefined || tableState.dataFrame !== sourceDataFrame) {
     771            0 :                 return state;
     772              :             }
     773            1 :             const current = kind === 'columns' ? tableState.columnSearch : tableState.dataSearch;
     774            1 :             if (current.requestId !== requestId) {
     775            1 :                 return state;
     776              :             }
     777            1 :             const nextSearch: ResultSearchState = {
     778            1 :                 ...current,
     779            1 :                 appliedPattern: '',
     780            1 :                 appliedRequestId: requestId,
     781            1 :                 matchingColumnGroups: null,
     782            1 :                 matchingRows: null,
     783            1 :                 pending: false,
     784            1 :                 error,
     785            1 :             };
     786            1 :             return {
     787            1 :                 ...state,
     788            1 :                 tableComputations: {
     789            1 :                     ...state.tableComputations,
     790            1 :                     [tableId]: {
     791            1 :                         ...tableState,
     792            1 :                         columnSearch: kind === 'columns' ? nextSearch : tableState.columnSearch,
     793            1 :                         dataSearch: kind === 'data' ? nextSearch : tableState.dataSearch,
     794            1 :                     }
     795            1 :                 }
     796            1 :             };
     797            1 :         }
     798            1 :     }
     799            1 : }
     800              : 
     801            1 : function updateTask(state: ComputationState, task: TaskVariant, progress: Partial<TaskProgress>, memory: DataFrameRegistry, create: boolean = false) {
     802            1 :     const allocatedTaskId = task.taskId !== undefined;
     803            1 :     if (!allocatedTaskId) {
     804            1 :         task = {
     805            1 :             ...task,
     806            1 :             taskId: state.nextSchedulerTaskId,
     807            1 :         };
     808              :     }
     809            1 :     const tableState = state.tableComputations[task.value.tableId];
     810            1 :     if (tableState === undefined) {
     811            0 :         return state;
     812              :     }
     813              : 
     814              :     // Note that we acquire data frames times twice for scheduler task and table computation state
     815            1 :     let registerTask: (tasks: TableComputationTasks) => TableComputationTasks = (t: TableComputationTasks) => t;
     816            1 :     let newStateVersion = tableState.version;
     817            1 :     switch (task.type) {
     818            1 :         case TABLE_FILTERING_TASK:
     819            1 :             if (create) {
     820            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     821              :                 // Increment filter version when scheduling (not when accepting result)
     822              :                 // This ensures concurrent filter tasks get unique target versions
     823            1 :                 newStateVersion = tableState.version.withFilterIncrement();
     824              :                 // Update the task itself with the new target version
     825            1 :                 task = {
     826            1 :                     ...task,
     827            1 :                     value: {
     828            1 :                         ...task.value,
     829            1 :                         tableVersion: newStateVersion,
     830            1 :                     }
     831            1 :                 };
     832            1 :             }
     833            1 :             registerTask = (tasks: TableComputationTasks) => {
     834            1 :                 const filterTask = task.value as TableFilteringTask;
     835            1 :                 return ({
     836            1 :                     ...tasks,
     837            1 :                     filteringTask: {
     838            1 :                         tableId: filterTask.tableId,
     839            1 :                         tableVersion: newStateVersion,
     840            1 :                         inputDataTable: filterTask.inputDataTable,
     841            1 :                         inputDataTableFieldIndex: filterTask.inputDataTableFieldIndex,
     842            1 :                         inputDataFrame: filterTask.inputDataFrame,
     843            1 :                         rowNumberColumnName: filterTask.rowNumberColumnName,
     844            1 :                         filters: filterTask.filters,
     845            1 :                         progress: {
     846            1 :                             ...tasks.filteringTask?.progress,
     847            1 :                             ...progress,
     848            1 :                         } as TaskProgress,
     849            1 :                     }
     850            1 :                 });
     851            1 :             };
     852            1 :             break;
     853            1 :         case TABLE_ORDERING_TASK:
     854            1 :             if (create) {
     855            1 :                 memory.release(tableState.tasks.orderingTask?.inputDataFrame);
     856            1 :                 memory.release(tableState.tasks.orderingTask?.filterTable?.dataFrame);
     857            1 :                 memory.release(tableState.tasks.orderingTask?.dataSearchTable?.dataFrame);
     858            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     859            1 :                 memory.acquire(task.value.filterTable?.dataFrame, 2);
     860            1 :                 memory.acquire(task.value.dataSearchTable?.dataFrame, 2);
     861            1 :             }
     862            1 :             registerTask = (tasks: TableComputationTasks) => {
     863            1 :                 const orderTask = task.value as TableOrderingTask;
     864            1 :                 if (
     865            1 :                     !create
     866            1 :                     && tasks.orderingTask != null
     867            1 :                     && !areOrderingConstraintsEqual(tasks.orderingTask.orderingConstraints, orderTask.orderingConstraints)
     868              :                 ) {
     869            0 :                     return tasks;
     870              :                 }
     871            1 :                 return ({
     872            1 :                     ...tasks,
     873            1 :                     orderingTask: {
     874            1 :                         tableId: orderTask.tableId,
     875            1 :                         tableVersion: tableState.version,
     876            1 :                         inputDataTable: orderTask.inputDataTable,
     877            1 :                         inputDataTableFieldIndex: orderTask.inputDataTableFieldIndex,
     878            1 :                         inputDataFrame: orderTask.inputDataFrame,
     879            1 :                         filterTable: orderTask.filterTable,
     880            1 :                         dataSearchTable: orderTask.dataSearchTable,
     881            1 :                         rowNumberColumnName: orderTask.rowNumberColumnName,
     882            1 :                         orderingConstraints: orderTask.orderingConstraints,
     883            1 :                         progress: {
     884            1 :                             ...tasks.orderingTask?.progress,
     885            1 :                             ...progress,
     886            1 :                         } as TaskProgress,
     887            1 :                     }
     888            1 :                 });
     889            1 :             };
     890            1 :             break;
     891            1 :         case DATA_SEARCH_TASK:
     892            1 :             if (create) {
     893            1 :                 tableState.tasks.dataSearchTask?.abortController.abort();
     894            1 :                 memory.release(tableState.tasks.dataSearchTask?.inputDataFrame);
     895            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     896            1 :             }
     897            1 :             registerTask = (tasks: TableComputationTasks) => {
     898            1 :                 const searchTask = task.value as DataSearchTask;
     899            1 :                 if (!create && tasks.dataSearchTask?.requestId !== searchTask.requestId) {
     900            1 :                     return tasks;
     901              :                 }
     902            1 :                 return {
     903            1 :                     ...tasks,
     904            1 :                     dataSearchTask: {
     905            1 :                         ...searchTask,
     906            1 :                         progress: {
     907            1 :                             ...tasks.dataSearchTask?.progress,
     908            1 :                             ...progress,
     909            1 :                         } as TaskProgress,
     910            1 :                     },
     911            1 :                 };
     912            1 :             };
     913            1 :             break;
     914            1 :         case TABLE_AGGREGATION_TASK:
     915            1 :             if (create) {
     916            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     917              :             }
     918            1 :             registerTask = (tasks: TableComputationTasks) => {
     919            1 :                 const aggTask = task.value as TableAggregationTask;
     920            1 :                 return ({
     921            1 :                     ...tasks,
     922            1 :                     tableAggregationTask: {
     923            1 :                         tableId: aggTask.tableId,
     924            1 :                         tableVersion: aggTask.tableVersion,
     925            1 :                         columnEntries: aggTask.columnEntries,
     926            1 :                         inputDataFrame: aggTask.inputDataFrame,
     927            1 :                         progress: {
     928            1 :                             ...tasks.tableAggregationTask?.progress,
     929            1 :                             ...progress,
     930            1 :                         } as TaskProgress,
     931            1 :                     }
     932            1 :                 });
     933            1 :             };
     934            1 :             break;
     935            1 :         case SYSTEM_COLUMN_COMPUTATION_TASK:
     936            1 :             if (create) {
     937            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     938            1 :                 memory.acquire(task.value.tableAggregate.dataFrame, 2);
     939            1 :             }
     940            1 :             registerTask = (tasks: TableComputationTasks) => {
     941            1 :                 const sysTask = task.value as SystemColumnComputationTask;
     942            1 :                 return ({
     943            1 :                     ...tasks,
     944            1 :                     systemColumnTask: {
     945            1 :                         tableId: sysTask.tableId,
     946            1 :                         tableVersion: sysTask.tableVersion,
     947            1 :                         columnEntries: sysTask.columnEntries,
     948            1 :                         inputTable: sysTask.inputTable,
     949            1 :                         inputDataFrame: sysTask.inputDataFrame,
     950            1 :                         rowNumberFieldName: sysTask.rowNumberFieldName,
     951            1 :                         tableAggregate: sysTask.tableAggregate,
     952            1 :                         progress: {
     953            1 :                             ...tasks.systemColumnTask?.progress,
     954            1 :                             ...progress,
     955            1 :                         } as TaskProgress,
     956            1 :                     }
     957            1 :                 });
     958            1 :             };
     959            1 :             break;
     960            1 :         case COLUMN_AGGREGATION_TASK:
     961            1 :             if (create) {
     962            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     963            1 :                 memory.acquire(task.value.tableAggregate.dataFrame, 2);
     964            1 :             }
     965            1 :             registerTask = (tasks: TableComputationTasks) => {
     966            1 :                 const colTask = task.value as ColumnAggregationTask;
     967            1 :                 const prev = tasks.columnAggregationTasks[colTask.columnId];
     968            1 :                 const updated = [...tasks.columnAggregationTasks];
     969            1 :                 updated[colTask.columnId] = {
     970            1 :                     tableId: colTask.tableId,
     971            1 :                     tableVersion: colTask.tableVersion,
     972            1 :                     columnId: colTask.columnId,
     973            1 :                     columnEntry: colTask.columnEntry,
     974            1 :                     inputDataFrame: colTask.inputDataFrame,
     975            1 :                     tableAggregate: colTask.tableAggregate,
     976            1 :                     progress: {
     977            1 :                         ...prev?.progress,
     978            1 :                         ...progress
     979            1 :                     } as TaskProgress
     980            1 :                 };
     981            1 :                 return ({
     982            1 :                     ...tasks,
     983            1 :                     columnAggregationTasks: updated
     984            1 :                 })
     985            1 :             };
     986            1 :             break;
     987            1 :         case FILTERED_COLUMN_AGGREGATION_TASK:
     988            1 :             if (create) {
     989            1 :                 const previous = tableState.tasks.filteredColumnAggregationTasks[task.value.columnId];
     990            1 :                 memory.release(previous?.inputDataFrame);
     991            1 :                 memory.release(previous?.filterTable?.dataFrame);
     992            1 :                 memory.release(previous?.dataSearchTable?.dataFrame);
     993            1 :                 releaseColumnAggregate(previous?.unfilteredAggregate, memory);
     994            1 :                 memory.acquire(task.value.inputDataFrame, 2);
     995            1 :                 memory.acquire(task.value.filterTable?.dataFrame, 2);
     996            1 :                 memory.acquire(task.value.dataSearchTable?.dataFrame, 2);
     997            1 :                 memory.acquire(task.value.tableAggregate.dataFrame, 2);
     998            1 :                 acquireColumnAggregate(task.value.unfilteredAggregate, memory, 2);
     999            1 :             }
    1000            1 :             registerTask = (tasks: TableComputationTasks) => {
    1001            1 :                 const filteredTask = task.value as WithFilter<ColumnAggregationTask>;
    1002            1 :                 const prev = tasks.filteredColumnAggregationTasks[filteredTask.columnId];
    1003            1 :                 const updated = [...tasks.filteredColumnAggregationTasks];
    1004            1 :                 updated[filteredTask.columnId] = {
    1005            1 :                     tableId: filteredTask.tableId,
    1006            1 :                     tableVersion: filteredTask.tableVersion,
    1007            1 :                     columnId: filteredTask.columnId,
    1008            1 :                     columnEntry: filteredTask.columnEntry,
    1009            1 :                     inputDataFrame: filteredTask.inputDataFrame,
    1010            1 :                     tableAggregate: filteredTask.tableAggregate,
    1011            1 :                     filterTable: filteredTask.filterTable,
    1012            1 :                     dataSearchTable: filteredTask.dataSearchTable,
    1013            1 :                     selectionRowCount: filteredTask.selectionRowCount,
    1014            1 :                     unfilteredAggregate: filteredTask.unfilteredAggregate,
    1015            1 :                     progress: {
    1016            1 :                         ...prev?.progress,
    1017            1 :                         ...progress
    1018            1 :                     } as TaskProgress
    1019            1 :                 };
    1020            1 :                 return ({
    1021            1 :                     ...tasks,
    1022            1 :                     filteredColumnAggregationTasks: updated
    1023            1 :                 })
    1024            1 :             };
    1025              :             break;
    1026            1 :     }
    1027            1 :     const updatedTaskId = task.taskId!;
    1028              :     const updatedState: ComputationState = {
    1029            1 :         ...state,
    1030            1 :         nextSchedulerTaskId: (!allocatedTaskId) ? (state.nextSchedulerTaskId + 1) : state.nextSchedulerTaskId,
    1031            1 :         schedulerTasks: {
    1032            1 :             ...state.schedulerTasks,
    1033            1 :             [updatedTaskId]: task
    1034            1 :         },
    1035            1 :         tableComputations: {
    1036            1 :             ...state.tableComputations,
    1037            1 :             [task.value.tableId]: {
    1038            1 :                 ...tableState,
    1039            1 :                 version: newStateVersion,
    1040            1 :                 tasks: registerTask(tableState.tasks)
    1041            1 :             }
    1042            1 :         }
    1043              :     };
    1044            1 :     return updatedState;
    1045            1 : }
    1046              : 
    1047            1 : function releaseTaskDataFrames(task: TaskVariant, memory: DataFrameRegistry) {
    1048            1 :     switch (task.type) {
    1049            1 :         case TABLE_FILTERING_TASK:
    1050            1 :         case TABLE_AGGREGATION_TASK:
    1051            1 :         case DATA_SEARCH_TASK:
    1052            1 :             memory.release(task.value.inputDataFrame);
    1053            1 :             break;
    1054            1 :         case TABLE_ORDERING_TASK:
    1055            1 :             memory.release(task.value.inputDataFrame);
    1056            1 :             memory.release(task.value.filterTable?.dataFrame);
    1057            1 :             memory.release(task.value.dataSearchTable?.dataFrame);
    1058            1 :             break;
    1059            1 :         case SYSTEM_COLUMN_COMPUTATION_TASK:
    1060            1 :             memory.release(task.value.inputDataFrame);
    1061            1 :             break;
    1062            1 :         case COLUMN_AGGREGATION_TASK:
    1063            1 :             memory.release(task.value.inputDataFrame);
    1064            1 :             break;
    1065            1 :         case FILTERED_COLUMN_AGGREGATION_TASK:
    1066            1 :             memory.release(task.value.inputDataFrame);
    1067            1 :             memory.release(task.value.filterTable?.dataFrame);
    1068            1 :             memory.release(task.value.dataSearchTable?.dataFrame);
    1069            1 :             releaseColumnAggregate(task.value.unfilteredAggregate, memory);
    1070              :             break;
    1071            1 :     }
    1072            1 : }
    1073              : 
    1074            1 : function areOrderingConstraintsEqual(left: OrderByConstraint[], right: OrderByConstraint[]): boolean {
    1075            1 :     if (left.length !== right.length) {
    1076            1 :         return false;
    1077              :     }
    1078            1 :     return left.every((constraint, index) => {
    1079            1 :         const other = right[index];
    1080            1 :         return constraint.field === other.field
    1081            1 :             && (constraint.ascending ?? true) === (other.ascending ?? true)
    1082            1 :             && (constraint.nullsFirst ?? false) === (other.nullsFirst ?? false);
    1083            1 :     });
    1084            1 : }
    1085              : 
    1086            1 : function tableFilteringSucceded(state: ComputationState, tableId: number, filterTable: FilterTable | null, memory: DataFrameRegistry) {
    1087            1 :     const tableState = state.tableComputations[tableId];
    1088            1 :     if (tableState === undefined) {
    1089            0 :         memory.release(filterTable?.dataFrame);
    1090            0 :         return state;
    1091            0 :     }
    1092              : 
    1093              :     // Mark filtering task succeeded
    1094            1 :     const filteringTask = !tableState.tasks.filteringTask ? null : {
    1095            1 :         ...tableState.tasks.filteringTask,
    1096            1 :         progress: {
    1097            1 :             ...tableState.tasks.filteringTask.progress,
    1098            1 :             status: TaskStatus.TASK_SUCCEEDED,
    1099            1 :             completedAt: new Date(),
    1100            1 :         }
    1101            1 :     };
    1102            1 :     const obsoleteFilteredColumnAggregates: Map<number, WithFilterEpoch<ColumnAggregationVariant> | null> = new Map();
    1103            1 :     let newFilteredColumnAggregatesOutdated = tableState.filteredColumnAggregatesOutdated;
    1104              : 
    1105            1 :     if (filterTable == null && tableState.dataSearchTable == null) {
    1106              :         // Clear column aggregates - track existing ones for release
    1107            1 :         for (let columnId = 0; columnId < tableState.filteredColumnAggregates.length; ++columnId) {
    1108            1 :             const prevAggregate = tableState.filteredColumnAggregates[columnId];
    1109            1 :             if (prevAggregate != null) {
    1110            0 :                 obsoleteFilteredColumnAggregates.set(columnId, prevAggregate);
    1111              :             }
    1112            1 :         }
    1113            1 :         newFilteredColumnAggregatesOutdated = Array.from({ length: tableState.filteredColumnAggregatesOutdated.length }, () => false);
    1114              : 
    1115            1 :     } else {
    1116            1 :         newFilteredColumnAggregatesOutdated = getFilteredColumnAggregatesOutdated(tableState, tableState.dataSearchTable, filterTable);
    1117              :     }
    1118              : 
    1119              :     // When the filter is cleared, release obsolete aggregates and clear the state.
    1120              :     // When the filter is active, we deliberately keep old aggregates until new ones are computed.
    1121              :     // We use the filter epoch to determine that an aggregate is not yet updated.
    1122            1 :     let newFilteredColumnAggregates = tableState.filteredColumnAggregates;
    1123            1 :     if (filterTable == null && tableState.dataSearchTable == null) {
    1124              :         // Release all obsolete filtered column aggregates
    1125            1 :         for (const [, prevAggregate] of obsoleteFilteredColumnAggregates.entries()) {
    1126            0 :             releaseColumnAggregate(prevAggregate, memory);
    1127              :         }
    1128              :         // Clear the filtered column aggregates array
    1129            1 :         newFilteredColumnAggregates = Array.from({ length: tableState.filteredColumnAggregates.length }, () => null);
    1130            1 :     }
    1131              : 
    1132            1 :     memory.release(tableState.filterTable?.dataFrame);
    1133            1 :     memory.release(tableState.orderingTable?.dataFrame);
    1134            1 :     return {
    1135            1 :         ...state,
    1136            1 :         tableComputations: {
    1137            1 :             ...state.tableComputations,
    1138            1 :             [tableId]: {
    1139            1 :                 ...tableState,
    1140              :                 // Don't increment version here - it was already incremented when scheduling the task
    1141              :                 // The filterTable.version should match the task's target version
    1142            1 :                 filterTable,
    1143            1 :                 orderingTable: null,
    1144            1 :                 filteredColumnAggregates: newFilteredColumnAggregates,
    1145            1 :                 filteredColumnAggregatesOutdated: newFilteredColumnAggregatesOutdated,
    1146            1 :                 tasks: {
    1147            1 :                     ...tableState.tasks,
    1148            1 :                     filteringTask,
    1149            1 :                 }
    1150            1 :             }
    1151            1 :         },
    1152            1 :     };
    1153            1 : }
    1154              : 
    1155            1 : function getFilteredColumnAggregatesOutdated(
    1156            1 :     tableState: TableComputationState,
    1157            1 :     dataSearchTable: DataSearchTable | null,
    1158            1 :     filterTable: FilterTable | null = tableState.filterTable,
    1159            1 : ): boolean[] {
    1160            1 :     if (filterTable == null && dataSearchTable == null) {
    1161            1 :         return Array.from({ length: tableState.filteredColumnAggregatesOutdated.length }, () => false);
    1162              :     }
    1163            1 :     const outdated = [...tableState.filteredColumnAggregatesOutdated];
    1164            1 :     const targetVersion = filterTable?.version ?? tableState.version;
    1165            1 :     for (let columnId = 0; columnId < tableState.columnGroups.length; ++columnId) {
    1166            1 :         const columnEntry = tableState.columnGroups[columnId];
    1167            1 :         if (columnEntry.type == SKIPPED_COLUMN || columnEntry.type == ROWNUMBER_COLUMN || columnEntry.type == LIST_COLUMN) {
    1168            1 :             outdated[columnId] = false;
    1169            1 :             continue;
    1170            1 :         }
    1171            1 :         const unfilteredAggregate = tableState.columnAggregates[columnId];
    1172            1 :         const previous = tableState.filteredColumnAggregates[columnId];
    1173            1 :         outdated[columnId] = unfilteredAggregate != null && !(
    1174            1 :             previous?.filterVersion.filterMatches(targetVersion)
    1175            0 :             && previous.dataSearchRequestId === (dataSearchTable?.requestId ?? null)
    1176              :         );
    1177            1 :     }
    1178            1 :     return outdated;
    1179            1 : }
    1180              : 
    1181              : /// Acquire a column aggregate
    1182            1 : function acquireColumnAggregate(aggregate: ColumnAggregationVariant | null | undefined, memory: DataFrameRegistry, times: number = 1) {
    1183            1 :     if (aggregate == null || aggregate == undefined) {
    1184            0 :         return;
    1185              :     }
    1186            1 :     switch (aggregate.type) {
    1187            1 :         case ORDINAL_COLUMN:
    1188            1 :             memory.acquire(aggregate.value.binnedDataFrame, times);
    1189            1 :             break;
    1190            1 :         case STRING_COLUMN:
    1191            1 :             memory.acquire(aggregate.value.frequentValuesDataFrame, times);
    1192            1 :             break;
    1193            0 :         case LIST_COLUMN:
    1194            0 :             memory.acquire(aggregate.value.frequentValuesDataFrame, times);
    1195            0 :             break;
    1196            0 :         case SKIPPED_COLUMN:
    1197              :             break;
    1198            1 :     }
    1199            1 : }
    1200              : 
    1201              : /// Helper to release memory of a column aggregate
    1202            1 : function releaseColumnAggregate(aggregate: ColumnAggregationVariant | null | undefined, memory: DataFrameRegistry) {
    1203            1 :     if (aggregate == null || aggregate == undefined) {
    1204            1 :         return;
    1205              :     }
    1206            1 :     switch (aggregate.type) {
    1207            1 :         case ORDINAL_COLUMN:
    1208            1 :             memory.release(aggregate.value.binnedDataFrame);
    1209            1 :             break;
    1210            1 :         case STRING_COLUMN:
    1211            1 :             memory.release(aggregate.value.frequentValuesDataFrame);
    1212            1 :             break;
    1213            0 :         case LIST_COLUMN:
    1214            0 :             memory.release(aggregate.value.frequentValuesDataFrame);
    1215            0 :             break;
    1216            0 :         case SKIPPED_COLUMN:
    1217              :             break;
    1218            1 :     }
    1219            1 : }
    1220              : 
    1221              : /// Helper to release memory of column aggregates
    1222            1 : function releaseColumnAggregates(aggregates: (ColumnAggregationVariant | null)[], dataFrameMemory: DataFrameRegistry) {
    1223            1 :     for (const aggregate of aggregates) {
    1224            1 :         if (aggregate != null) {
    1225            0 :             releaseColumnAggregate(aggregate, dataFrameMemory);
    1226              :         }
    1227              :     }
    1228            1 : }
    1229              : 
    1230              : /// Helper to destroy state of a table
    1231            1 : function destroyTableComputationState(state: TableComputationState, memory: DataFrameRegistry) {
    1232            1 :     state.dataTableLifetime.abort();
    1233              :     // Release data frames
    1234            1 :     memory.release(state.dataFrame);
    1235            1 :     memory.release(state.filterTable?.dataFrame);
    1236            1 :     memory.release(state.dataSearchTable?.dataFrame);
    1237            1 :     memory.release(state.orderingTable?.dataFrame);
    1238            1 :     memory.release(state.tableAggregation?.dataFrame);
    1239            1 :     releaseColumnAggregates(state.columnAggregates, memory);
    1240            1 :     releaseColumnAggregates(state.filteredColumnAggregates, memory);
    1241              : 
    1242              :     // Release data frames from tasks
    1243            1 :     memory.release(state.tasks.filteringTask?.inputDataFrame);
    1244            1 :     memory.release(state.tasks.orderingTask?.inputDataFrame);
    1245            1 :     memory.release(state.tasks.orderingTask?.filterTable?.dataFrame);
    1246            1 :     memory.release(state.tasks.orderingTask?.dataSearchTable?.dataFrame);
    1247            1 :     memory.release(state.tasks.dataSearchTask?.inputDataFrame);
    1248            1 :     memory.release(state.tasks.tableAggregationTask?.inputDataFrame);
    1249            1 :     memory.release(state.tasks.systemColumnTask?.inputDataFrame);
    1250            1 :     memory.releaseMany(state.tasks.columnAggregationTasks.map(task => task?.inputDataFrame));
    1251            1 :     for (const task of state.tasks.filteredColumnAggregationTasks) {
    1252            1 :         memory.release(task?.filterTable?.dataFrame);
    1253            1 :         memory.release(task?.dataSearchTable?.dataFrame);
    1254            1 :         releaseColumnAggregate(task?.unfilteredAggregate ?? null, memory);
    1255            1 :     }
    1256            1 : }
        

Generated by: LCOV version 2.0-1