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

            Line data    Source code
       1              : import { ArrowTableFormatter } from '../arrow_formatter.js';
       2              : import { DataFrame, generateTableName } from '../data_frame.js';
       3              : import { ColumnAggregationVariant, ColumnAggregationTask, ORDINAL_COLUMN, STRING_COLUMN, LIST_COLUMN, SKIPPED_COLUMN, ROWNUMBER_COLUMN, getGridColumnTypeName, WithFilter, WithFilterEpoch } from '../computation_types.js';
       4              : import { LoggableException, LoggerLike, stringifyError } from '../../platform/logger/logger.js';
       5              : import { OrderByConstraint, SQLFrame } from '../sql/sqlframe_builder.js';
       6              : import { ComputeQueryExecution, executeDirectly } from '../computation_tasks.js';
       7              : import { getToNumericFn, isIntegerType } from '../arrow_columns.js';
       8              : import { analyzeOrdinalColumn, analyzeStringColumn } from '../column_analysis.js';
       9              : 
      10            1 : const LOG_CTX = "compute";
      11              : 
      12              : /// The order of the bins of an ordinal column
      13            1 : const BIN_ORDER: OrderByConstraint[] = [{ field: "bin", ascending: true }];
      14              : /// The order of the frequent values of a string column, most frequent first
      15            1 : const FREQUENT_VALUE_ORDER: OrderByConstraint[] = [{ field: "count", ascending: false }];
      16              : /// The number of frequent values of a string column
      17            1 : const FREQUENT_VALUE_COUNT = 32;
      18              : 
      19            1 : export async function computeColumnAggregates(task: ColumnAggregationTask, logger: LoggerLike, executeQuery: ComputeQueryExecution = executeDirectly): Promise<ColumnAggregationVariant> {
      20            1 :     if (task.columnEntry.type == SKIPPED_COLUMN || task.columnEntry.type == ROWNUMBER_COLUMN || task.columnEntry.type == LIST_COLUMN) {
      21            0 :         throw new LoggableException(`Column of type cannot be aggregated`, {
      22            0 :             type: getGridColumnTypeName(task.columnEntry),
      23            0 :         }, LOG_CTX);
      24              :     }
      25              : 
      26            1 :     const [sql, orderBy] = buildColumnAggregationSQL(task);
      27              : 
      28            1 :     try {
      29            1 :         const transformStart = performance.now();
      30            1 :         const tableName = generateTableName("__col_agg");
      31            1 :         const aggregateDataFrame = await executeQuery(sql, clientContext => DataFrame.fromSQL(task.inputDataFrame.database, sql, tableName, undefined, clientContext));
      32            1 :         const transformEnd = performance.now();
      33              : 
      34            1 :         const aggregateTable = await aggregateDataFrame.readTable({ orderBy });
      35            1 :         const aggregateTableFormatter = new ArrowTableFormatter(aggregateTable.schema, aggregateTable.batches, logger);
      36            1 :         const readEnd = performance.now();
      37              : 
      38            1 :         let columnAggregate: ColumnAggregationVariant;
      39            1 :         switch (task.columnEntry.type) {
      40            1 :             case ORDINAL_COLUMN: {
      41            1 :                 const analysis = analyzeOrdinalColumn(task.tableAggregate, task.columnEntry.value, aggregateTable, aggregateTableFormatter);
      42            1 :                 columnAggregate = {
      43            1 :                     type: ORDINAL_COLUMN,
      44            1 :                     value: {
      45            1 :                         columnEntry: task.columnEntry.value,
      46            1 :                         binnedDataFrame: aggregateDataFrame,
      47            1 :                         binnedValues: aggregateTable,
      48            1 :                         binnedValuesFormatter: aggregateTableFormatter,
      49            1 :                         columnAnalysis: analysis,
      50            1 :                     }
      51            1 :                 };
      52            1 :                 break;
      53            1 :             }
      54            1 :             case STRING_COLUMN: {
      55            1 :                 const analysis = analyzeStringColumn(task.tableAggregate, task.columnEntry.value, aggregateTable, aggregateTableFormatter);
      56            1 :                 columnAggregate = {
      57            1 :                     type: STRING_COLUMN,
      58            1 :                     value: {
      59            1 :                         columnEntry: task.columnEntry.value,
      60            1 :                         frequentValuesDataFrame: aggregateDataFrame,
      61            1 :                         frequentValuesTable: aggregateTable,
      62            1 :                         frequentValuesFormatter: aggregateTableFormatter,
      63            1 :                         analysis,
      64            1 :                     }
      65            1 :                 };
      66            1 :                 break;
      67            1 :             }
      68            1 :         }
      69            1 :         const analysisEnd = performance.now();
      70            1 :         logger.info("Aggregated table column", {
      71            1 :             "version": task.tableVersion.toString(),
      72            1 :             "table": task.tableId.toString(),
      73            1 :             "columnIndex": task.columnId.toString(),
      74            1 :             "columnName": task.columnEntry.value.inputFieldName,
      75            1 :             "groupType": getGridColumnTypeName(task.columnEntry),
      76            1 :             "duration": Math.floor(analysisEnd - transformStart).toString(),
      77            1 :             "queryDuration": Math.floor(transformEnd - transformStart).toString(),
      78            1 :             "readDuration": Math.floor(readEnd - transformEnd).toString(),
      79            1 :             "analysisDuration": Math.floor(analysisEnd - readEnd).toString(),
      80            1 :             "resultRows": aggregateTable.numRows.toString(),
      81            1 :             "sql": sql,
      82            1 :         }, LOG_CTX);
      83            1 :         return columnAggregate;
      84              : 
      85            1 :     } catch (error: any) {
      86            0 :         if (error instanceof LoggableException) {
      87            0 :             throw error;
      88            0 :         } else {
      89            0 :             const exception = new LoggableException("Computing column aggregate failed", {
      90            0 :                 "table": task.tableId.toString(),
      91            0 :                 "error": stringifyError(error),
      92            0 :             }, LOG_CTX);
      93            0 :             logger.warn(exception.message, exception.keyValues, exception.target);
      94            0 :             throw exception;
      95            0 :         }
      96            0 :     }
      97            1 : }
      98              : 
      99              : /// The SQL of the aggregates of a column, and the order of their rows
     100            1 : function buildColumnAggregationSQL(task: ColumnAggregationTask, filtered: WithFilter<ColumnAggregationTask> | null = null): [string, OrderByConstraint[]] {
     101            1 :     if (task.columnEntry.type == SKIPPED_COLUMN || task.columnEntry.type == ROWNUMBER_COLUMN || task.columnEntry.value.statsFields == null) {
     102            0 :         throw new Error("Column summary requires precomputed table summary");
     103              :     }
     104              : 
     105            1 :     let frame = SQLFrame.from(task.inputDataFrame.tableName);
     106              : 
     107            1 :     if (filtered != null) {
     108            1 :         if (filtered.filterTable != null) {
     109            1 :             frame = frame.semiJoinFilter(
     110            1 :                 filtered.filterTable.inputRowNumberColumnName,
     111            1 :                 filtered.filterTable.dataFrame.tableName,
     112            1 :                 filtered.filterTable.dataTable.schema.fields[0].name,
     113            1 :             );
     114              :         }
     115            1 :         if (filtered.dataSearchTable != null) {
     116            1 :             frame = frame.semiJoinFilter(
     117            1 :                 filtered.dataSearchTable.inputRowNumberColumnName,
     118            1 :                 filtered.dataSearchTable.dataFrame.tableName,
     119            1 :                 filtered.dataSearchTable.dataTable.schema.fields[0].name,
     120            1 :             );
     121              :         }
     122            1 :     }
     123              : 
     124            1 :     const targetFieldName = task.columnEntry.value.inputFieldName;
     125              : 
     126            1 :     switch (task.columnEntry.type) {
     127            1 :         case ORDINAL_COLUMN: {
     128            1 :             const minField = task.columnEntry.value.statsFields.minAggregateFieldName!;
     129            1 :             const maxField = task.columnEntry.value.statsFields.maxAggregateFieldName!;
     130            1 :             frame = frame.groupBy({
     131            1 :                 keys: [{
     132            1 :                     fieldName: targetFieldName,
     133            1 :                     outputAlias: "bin",
     134            1 :                     binning: {
     135            1 :                         preBinnedFieldName: task.columnEntry.value.binFieldName ?? undefined,
     136            1 :                         statsTable: task.tableAggregate.dataFrame.tableName,
     137            1 :                         statsMinField: minField,
     138            1 :                         statsMaxField: maxField,
     139            1 :                         binCount: task.columnEntry.value.binCount,
     140            1 :                         minBinWidth: isIntegerType(task.columnEntry.value.inputFieldType.typeId) ? 1 : undefined,
     141            1 :                         outputBinWidthAlias: "binWidth",
     142            1 :                         outputBinLbAlias: "binLowerBound",
     143            1 :                         outputBinUbAlias: "binUpperBound",
     144            1 :                         includeNullBin: true,
     145            1 :                         toNumericFn: getToNumericFn(task.columnEntry.value.inputFieldType.typeId),
     146            1 :                     }
     147            1 :                 }],
     148            1 :                 aggregates: [{
     149            1 :                     func: "count_star",
     150            1 :                     outputAlias: "count",
     151            1 :                 }]
     152            1 :             });
     153            1 :             return [frame.toSQL(), BIN_ORDER];
     154            1 :         }
     155            1 :         case STRING_COLUMN: {
     156            1 :             if (task.columnEntry.value.valueIdFieldName == null) {
     157            0 :                 throw new Error("Cannot aggregate string column without precomputed value id");
     158              :             }
     159            1 :             frame = frame.groupBy({
     160            1 :                 keys: [
     161            1 :                     { fieldName: targetFieldName, outputAlias: "key" },
     162            1 :                     { fieldName: task.columnEntry.value.valueIdFieldName, outputAlias: "keyId" },
     163            1 :                 ],
     164            1 :                 aggregates: [{
     165            1 :                     func: "count_star",
     166            1 :                     outputAlias: "count",
     167            1 :                 }]
     168            1 :             }).orderBy(FREQUENT_VALUE_ORDER, FREQUENT_VALUE_COUNT);
     169            1 :             return [frame.toSQL(), FREQUENT_VALUE_ORDER];
     170              :         }
     171            1 :     }
     172              : 
     173            0 :     return [frame.toSQL(), []];
     174            1 : }
     175              : 
     176            1 : export async function computeFilteredColumnAggregates(task: WithFilter<ColumnAggregationTask>, logger: LoggerLike, executeQuery: ComputeQueryExecution = executeDirectly): Promise<WithFilterEpoch<ColumnAggregationVariant> | null> {
     177            1 :     if (task.columnEntry.type == SKIPPED_COLUMN || task.columnEntry.type == ROWNUMBER_COLUMN || task.columnEntry.type == LIST_COLUMN) {
     178            1 :         throw new LoggableException(`Column of type cannot be aggregated`, {
     179            1 :             type: getGridColumnTypeName(task.columnEntry),
     180            1 :         }, LOG_CTX);
     181              :     }
     182              : 
     183            1 :     const [sql, orderBy] = buildColumnAggregationSQL(task, task);
     184              : 
     185            1 :     try {
     186            1 :         logger.info("Aggregating filtered table column", {
     187            1 :             "version": task.tableVersion.toString(),
     188            1 :             "table": task.tableId.toString(),
     189            1 :             "columnIndex": task.columnId.toString(),
     190            1 :             "columnName": task.columnEntry.value.inputFieldName,
     191            1 :             "groupType": getGridColumnTypeName(task.columnEntry),
     192            1 :         }, LOG_CTX);
     193            1 :         const transformStart = performance.now();
     194            1 :         const tableName = generateTableName("__filt_col_agg");
     195            1 :         const aggregateDataFrame = await executeQuery(sql, clientContext => DataFrame.fromSQL(task.inputDataFrame.database, sql, tableName, undefined, clientContext));
     196            1 :         const transformEnd = performance.now();
     197            1 :         logger.info("Aggregated filtered table column", {
     198            1 :             "version": task.tableVersion.toString(),
     199            1 :             "table": task.tableId.toString(),
     200            1 :             "columnIndex": task.columnId.toString(),
     201            1 :             "columnName": task.columnEntry.value.inputFieldName,
     202            1 :             "groupType": getGridColumnTypeName(task.columnEntry),
     203            1 :             "duration": Math.floor(transformEnd - transformStart).toString(),
     204            1 :             "sql": sql
     205            1 :         }, LOG_CTX);
     206              : 
     207            1 :         const aggregateTable = await aggregateDataFrame.readTable({ orderBy });
     208            1 :         const aggregateTableFormatter = new ArrowTableFormatter(aggregateTable.schema, aggregateTable.batches, logger);
     209              : 
     210            1 :         let columnAggregate: WithFilterEpoch<ColumnAggregationVariant>;
     211            1 :         switch (task.columnEntry.type) {
     212            1 :             case ORDINAL_COLUMN: {
     213            1 :                 const analysis = analyzeOrdinalColumn(task.tableAggregate, task.columnEntry.value, aggregateTable, aggregateTableFormatter, task.selectionRowCount);
     214            1 :                 columnAggregate = {
     215            1 :                     type: ORDINAL_COLUMN,
     216            1 :                     value: {
     217            1 :                         columnEntry: task.columnEntry.value,
     218            1 :                         binnedDataFrame: aggregateDataFrame,
     219            1 :                         binnedValues: aggregateTable,
     220            1 :                         binnedValuesFormatter: aggregateTableFormatter,
     221            1 :                         columnAnalysis: analysis,
     222            1 :                     },
     223            1 :                     filterVersion: task.tableVersion.clone(),
     224            1 :                     dataSearchRequestId: task.dataSearchTable?.requestId ?? null,
     225            1 :                 };
     226            1 :                 break;
     227            1 :             }
     228            1 :             case STRING_COLUMN: {
     229            1 :                 const analysis = analyzeStringColumn(task.tableAggregate, task.columnEntry.value, aggregateTable, aggregateTableFormatter, task.selectionRowCount);
     230            1 :                 columnAggregate = {
     231            1 :                     type: STRING_COLUMN,
     232            1 :                     value: {
     233            1 :                         columnEntry: task.columnEntry.value,
     234            1 :                         frequentValuesDataFrame: aggregateDataFrame,
     235            1 :                         frequentValuesTable: aggregateTable,
     236            1 :                         frequentValuesFormatter: aggregateTableFormatter,
     237            1 :                         analysis,
     238            1 :                     },
     239            1 :                     filterVersion: task.tableVersion.clone(),
     240            1 :                     dataSearchRequestId: task.dataSearchTable?.requestId ?? null,
     241            1 :                 };
     242            1 :                 break;
     243            1 :             }
     244            1 :         }
     245            1 :         return columnAggregate;
     246              : 
     247            1 :     } catch (error: any) {
     248            1 :         if (error instanceof LoggableException) {
     249            0 :             throw error;
     250            1 :         } else {
     251            1 :             const exception = new LoggableException("Failed to compute filtered column aggregate", {
     252            1 :                 "table": task.tableId.toString(),
     253            1 :                 "columnIndex": task.columnId.toString(),
     254            1 :                 "columnName": task.columnEntry.value.inputFieldName,
     255            1 :                 "error": stringifyError(error),
     256            1 :             }, LOG_CTX);
     257            1 :             logger.warn(exception.message, exception.keyValues, exception.target);
     258            1 :             throw exception;
     259            1 :         }
     260            1 :     }
     261            1 : }
        

Generated by: LCOV version 2.0-1