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

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : 
       3              : import { TableComputationState } from "../../compute/computation_state.js";
       4              : import { ArrowTableFormatter } from "../../compute/arrow_formatter.js";
       5              : import { ColumnGroup, LIST_COLUMN, ORDINAL_COLUMN, ROWNUMBER_COLUMN, SKIPPED_COLUMN, STRING_COLUMN } from "../../compute/computation_types.js";
       6              : 
       7              : export interface DataTableLayout {
       8              :     columnCount: number;
       9              :     arrowFieldByColumnIndex: Uint32Array;
      10              :     columnXOffsets: Float64Array;
      11              :     columnAggregateByColumnIndex: Int32Array;
      12              :     columnGroupByColumnIndex: Uint32Array;
      13              :     isSystemColumn: Uint8Array;
      14              :     isTextColumn: Uint8Array;
      15              :     headerRowCount: number;
      16              : }
      17              : 
      18            1 : const MAX_VALUE_COLUMN_WIDTH = 300;
      19            1 : const MIN_COLUMN_WIDTH = 120;
      20            1 : const COLUMN_HEADER_ACTION_WIDTH = 24;
      21            1 : const ROW_HEADER_WIDTH = 48;
      22            1 : const FORMATTER_PIXEL_SCALING = 10;
      23              : 
      24            1 : function computeColumnCount(columnGroups: ColumnGroup[], showMetaColumns: boolean, visibleColumnGroups?: ReadonlySet<number> | null): number {
      25            1 :     let columnCount = 0;
      26            1 :     for (let groupIndex = 0; groupIndex < columnGroups.length; ++groupIndex) {
      27            1 :         const columnGroup = columnGroups[groupIndex];
      28            1 :         if (columnGroup.type !== ROWNUMBER_COLUMN && visibleColumnGroups != null && !visibleColumnGroups.has(groupIndex)) {
      29            1 :             continue;
      30              :         }
      31            1 :         switch (columnGroup.type) {
      32            1 :             case ROWNUMBER_COLUMN:
      33            1 :                 ++columnCount;
      34            1 :                 break;
      35            0 :             case SKIPPED_COLUMN:
      36            0 :                 break;
      37            1 :             case STRING_COLUMN:
      38            1 :             case LIST_COLUMN:
      39            1 :                 ++columnCount;
      40            1 :                 if (showMetaColumns && columnGroup.value.valueIdFieldName != null) {
      41            1 :                     ++columnCount;
      42              :                 }
      43            1 :                 break;
      44            1 :             case ORDINAL_COLUMN:
      45            1 :                 ++columnCount;
      46            1 :                 if (showMetaColumns && columnGroup.value.binFieldName != null) {
      47            1 :                     ++columnCount;
      48              :                 }
      49              :                 break;
      50            1 :         }
      51            1 :     }
      52            1 :     return columnCount;
      53            1 : }
      54              : 
      55            1 : export function computeTableLayout(formatter: ArrowTableFormatter, state: TableComputationState, showSystemColumns: boolean, headerRowCount: number, containerWidth: number = 0, visibleColumnGroups?: ReadonlySet<number> | null, resizedColumnWidths?: ReadonlyMap<string, number> | null): DataTableLayout {
      56              :     // Allocate column offsets
      57            1 :     let columnCount = computeColumnCount(state.columnGroups, showSystemColumns, visibleColumnGroups);
      58            1 :     const columnFields = new Uint32Array(columnCount);
      59            1 :     const columnOffsets = new Float64Array(columnCount + 1);
      60            1 :     const columnAggregateIndex = new Int32Array(columnCount);
      61            1 :     const columnGroupByColumnIndex = new Uint32Array(columnCount);
      62            1 :     const isSystemColumn = new Uint8Array(columnCount);
      63            1 :     const isTextColumn = new Uint8Array(columnCount);
      64            1 :     const tableSchema = state.dataTable.schema;
      65              : 
      66              :     // Index table fields by name
      67            1 :     const fieldIndexByName = new Map<string, number>();
      68            1 :     for (let i = 0; i < tableSchema.fields.length; ++i) {
      69            1 :         fieldIndexByName.set(tableSchema.fields[i].name, i);
      70              :     }
      71              : 
      72            1 :     for (let i = 0; i < columnCount; ++i) {
      73            1 :         columnAggregateIndex[i] = -1;
      74              :     }
      75              : 
      76              :     // Allocate column offsets
      77            1 :     let nextDisplayColumn = 0;
      78            1 :     let nextDisplayOffset = 0;
      79            1 :     for (let groupIndex = 0; groupIndex < state.columnGroups.length; ++groupIndex) {
      80            1 :         const columnGroup = state.columnGroups[groupIndex];
      81            1 :         if (columnGroup.type !== ROWNUMBER_COLUMN && visibleColumnGroups != null && !visibleColumnGroups.has(groupIndex)) {
      82            1 :             continue;
      83              :         }
      84            1 :         switch (columnGroup.type) {
      85            1 :             case ROWNUMBER_COLUMN: {
      86            1 :                 const outputIndex = nextDisplayColumn++;
      87            1 :                 columnFields[outputIndex] = fieldIndexByName.get(columnGroup.value.rowNumberFieldName)!;
      88            1 :                 columnOffsets[outputIndex] = nextDisplayOffset;
      89            1 :                 columnGroupByColumnIndex[outputIndex] = groupIndex;
      90            1 :                 nextDisplayOffset += ROW_HEADER_WIDTH;
      91            1 :                 break;
      92            1 :             }
      93            0 :             case SKIPPED_COLUMN:
      94            0 :                 break;
      95            1 :             case ORDINAL_COLUMN:
      96            1 :                 const outputIndex = nextDisplayColumn++;
      97            1 :                 const valueColumnFormatter = formatter.columns[fieldIndexByName.get(columnGroup.value.inputFieldName)!];
      98            1 :                 let valueColumnWidth = Math.max(
      99            1 :                     COLUMN_HEADER_ACTION_WIDTH + Math.max(
     100            1 :                         valueColumnFormatter.getLayoutInfo().valueAvgWidth,
     101            1 :                         valueColumnFormatter.getColumnName().length) * FORMATTER_PIXEL_SCALING,
     102            1 :                     MIN_COLUMN_WIDTH
     103            1 :                 );
     104            1 :                 valueColumnWidth = Math.min(valueColumnWidth, MAX_VALUE_COLUMN_WIDTH);
     105            1 :                 columnFields[outputIndex] = fieldIndexByName.get(columnGroup.value.inputFieldName)!;
     106            1 :                 columnOffsets[outputIndex] = nextDisplayOffset;
     107            1 :                 columnAggregateIndex[outputIndex] = groupIndex;
     108            1 :                 columnGroupByColumnIndex[outputIndex] = groupIndex;
     109            1 :                 nextDisplayOffset += valueColumnWidth;
     110            1 :                 if (showSystemColumns && columnGroup.value.binFieldName != null) {
     111            1 :                     const idOutputIndex = nextDisplayColumn++;
     112            1 :                     const idFieldIndex = fieldIndexByName.get(columnGroup.value.binFieldName)!;
     113            1 :                     const idColumn = formatter.columns[idFieldIndex];
     114            1 :                     const idColumnWidth = Math.max(
     115            1 :                         COLUMN_HEADER_ACTION_WIDTH + Math.max(
     116            1 :                             idColumn.getLayoutInfo().valueAvgWidth,
     117            1 :                             idColumn.getColumnName().length) * FORMATTER_PIXEL_SCALING,
     118            1 :                         MIN_COLUMN_WIDTH
     119            1 :                     );
     120            1 :                     columnFields[idOutputIndex] = idFieldIndex;
     121            1 :                     columnOffsets[idOutputIndex] = nextDisplayOffset;
     122            1 :                     columnGroupByColumnIndex[idOutputIndex] = groupIndex;
     123            1 :                     isSystemColumn[idOutputIndex] = 1;
     124            1 :                     nextDisplayOffset += idColumnWidth;
     125            1 :                 }
     126            1 :                 break;
     127            1 :             case STRING_COLUMN:
     128            1 :             case LIST_COLUMN: {
     129            1 :                 const outputIndex = nextDisplayColumn++;
     130            1 :                 const valueColumnFormatter = formatter.columns[fieldIndexByName.get(columnGroup.value.inputFieldName)!];
     131            1 :                 let valueColumnWidth = Math.max(
     132            1 :                     COLUMN_HEADER_ACTION_WIDTH + Math.max(
     133            1 :                         valueColumnFormatter.getLayoutInfo().valueAvgWidth,
     134            1 :                         valueColumnFormatter.getColumnName().length) * FORMATTER_PIXEL_SCALING,
     135            1 :                     MIN_COLUMN_WIDTH
     136            1 :                 );
     137            1 :                 valueColumnWidth = Math.min(valueColumnWidth, MAX_VALUE_COLUMN_WIDTH);
     138            1 :                 columnFields[outputIndex] = fieldIndexByName.get(columnGroup.value.inputFieldName)!;
     139            1 :                 columnOffsets[outputIndex] = nextDisplayOffset;
     140            1 :                 columnAggregateIndex[outputIndex] = groupIndex;
     141            1 :                 columnGroupByColumnIndex[outputIndex] = groupIndex;
     142            1 :                 nextDisplayOffset += valueColumnWidth;
     143            1 :                 if (showSystemColumns && columnGroup.value.valueIdFieldName != null) {
     144            1 :                     const idOutputIndex = nextDisplayColumn++;
     145            1 :                     const idFieldIndex = fieldIndexByName.get(columnGroup.value.valueIdFieldName)!;
     146            1 :                     const idColumn = formatter.columns[idFieldIndex];
     147            1 :                     const idColumnWidth = Math.max(
     148            1 :                         COLUMN_HEADER_ACTION_WIDTH + Math.max(
     149            1 :                             idColumn.getLayoutInfo().valueAvgWidth,
     150            1 :                             idColumn.getColumnName().length) * FORMATTER_PIXEL_SCALING,
     151            1 :                         MIN_COLUMN_WIDTH
     152            1 :                     );
     153            1 :                     columnFields[idOutputIndex] = idFieldIndex;
     154            1 :                     columnOffsets[idOutputIndex] = nextDisplayOffset;
     155            1 :                     columnGroupByColumnIndex[idOutputIndex] = groupIndex;
     156            1 :                     isSystemColumn[idOutputIndex] = 1;
     157            1 :                     nextDisplayOffset += idColumnWidth;
     158            1 :                 }
     159            1 :                 break;
     160            1 :             }
     161            1 :         }
     162            1 :     }
     163            1 :     columnOffsets[nextDisplayColumn] = nextDisplayOffset;
     164              : 
     165              :     // Mark text columns for format peek hints
     166            1 :     for (let i = 0; i < nextDisplayColumn; ++i) {
     167            1 :         const typeId = tableSchema.fields[columnFields[i]].type.typeId;
     168            1 :         if (typeId === arrow.Type.Utf8 || typeId === arrow.Type.LargeUtf8) {
     169            1 :             isTextColumn[i] = 1;
     170              :         }
     171            1 :     }
     172              : 
     173              :     // Replace the widths of the columns that the user resized, keyed by field name
     174            1 :     const isResizedColumn = new Uint8Array(columnCount);
     175            1 :     if (resizedColumnWidths != null && resizedColumnWidths.size > 0) {
     176            1 :         let naturalStart = columnOffsets[0];
     177            1 :         for (let i = 0; i < nextDisplayColumn; ++i) {
     178            1 :             const naturalEnd = columnOffsets[i + 1];
     179            1 :             const resizedWidth = resizedColumnWidths.get(tableSchema.fields[columnFields[i]].name);
     180            1 :             let width = naturalEnd - naturalStart;
     181            1 :             if (resizedWidth !== undefined) {
     182            1 :                 width = resizedWidth;
     183            1 :                 isResizedColumn[i] = 1;
     184            1 :             }
     185            1 :             columnOffsets[i + 1] = columnOffsets[i] + width;
     186            1 :             naturalStart = naturalEnd;
     187            1 :         }
     188            1 :         nextDisplayOffset = columnOffsets[nextDisplayColumn];
     189            1 :     }
     190              : 
     191              :     // If columns don't fill the container, distribute extra space across data columns (not row-number or resized columns)
     192            1 :     if (containerWidth > 0 && nextDisplayOffset < containerWidth) {
     193            1 :         const stretchesColumn = (i: number) => isResizedColumn[i] == 0
     194            1 :             && state.columnGroups[columnGroupByColumnIndex[i]].type !== ROWNUMBER_COLUMN;
     195            1 :         let dataColumnCount = 0;
     196            1 :         for (let i = 0; i < nextDisplayColumn; ++i) {
     197            1 :             if (stretchesColumn(i)) {
     198            1 :                 dataColumnCount++;
     199              :             }
     200              :         }
     201            1 :         if (dataColumnCount > 0) {
     202            1 :             const extraPerColumn = (containerWidth - nextDisplayOffset) / dataColumnCount;
     203            1 :             let accumulatedExtra = 0;
     204            1 :             for (let i = 0; i < nextDisplayColumn; ++i) {
     205            1 :                 if (stretchesColumn(i)) {
     206            1 :                     accumulatedExtra += extraPerColumn;
     207              :                 }
     208            1 :                 columnOffsets[i + 1] += accumulatedExtra;
     209            1 :             }
     210            1 :         }
     211            1 :     }
     212              : 
     213            1 :     return {
     214            1 :         columnCount,
     215            1 :         arrowFieldByColumnIndex: columnFields,
     216            1 :         columnXOffsets: columnOffsets,
     217            1 :         columnAggregateByColumnIndex: columnAggregateIndex,
     218            1 :         columnGroupByColumnIndex: columnGroupByColumnIndex,
     219            1 :         isSystemColumn: isSystemColumn,
     220            1 :         isTextColumn: isTextColumn,
     221            1 :         headerRowCount
     222            1 :     };
     223            1 : }
     224              : 
     225              : export function skipTableLayoutUpdate(old: DataTableLayout, next: DataTableLayout) {
     226              :     if (old.columnXOffsets.length != next.columnXOffsets.length) {
     227              :         return false;
     228              :     }
     229              :     for (let i = 0; i < old.columnXOffsets.length; ++i) {
     230              :         const delta = next.columnXOffsets[i] - old.columnXOffsets[i];
     231              :         if (Math.abs(delta) > 0.01) {
     232              :             return false;
     233              :         }
     234              :     }
     235              :     return true;
     236              : }
        

Generated by: LCOV version 2.0-1