LCOV - code coverage report
Current view: top level - dashql-app/src/compute/sql - query_result_search_sql.ts Coverage Total Hit
Test: DashQL 915828b Lines: 96.3 % 81 78
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as arrow from 'apache-arrow';
       2              : 
       3              : import { quoteIdent, quoteStringLiteral } from './sqlframe_builder.js';
       4              : import { ColumnGroup, LIST_COLUMN, ORDINAL_COLUMN, ROWNUMBER_COLUMN, SKIPPED_COLUMN, STRING_COLUMN } from '../computation_types.js';
       5              : 
       6              : /// The row number of a matching row in the result of buildDataSearchSQL()
       7            1 : export const DATA_SEARCH_ROW_FIELD = 'row_idx';
       8              : 
       9              : export interface SearchableResultColumn {
      10              :     /// Display-column index among original result columns, excluding the row-number column.
      11              :     columnIdx: number;
      12              :     /// Index into `TableComputationState.columnGroups`.
      13              :     columnGroupIdx: number;
      14              :     /// Arrow field name of the original result column.
      15              :     fieldName: string;
      16              : }
      17              : 
      18            1 : export function collectSearchableResultColumns(columnGroups: ColumnGroup[]): SearchableResultColumn[] {
      19            1 :     const columns: SearchableResultColumn[] = [];
      20            1 :     for (let columnGroupIdx = 0; columnGroupIdx < columnGroups.length; ++columnGroupIdx) {
      21            1 :         const columnGroup = columnGroups[columnGroupIdx];
      22            1 :         switch (columnGroup.type) {
      23            1 :             case ROWNUMBER_COLUMN:
      24            1 :             case SKIPPED_COLUMN:
      25            1 :                 break;
      26            1 :             case ORDINAL_COLUMN:
      27            1 :             case STRING_COLUMN:
      28            1 :             case LIST_COLUMN:
      29            1 :                 columns.push({
      30            1 :                     columnIdx: columns.length,
      31            1 :                     columnGroupIdx,
      32            1 :                     fieldName: columnGroup.value.inputFieldName,
      33            1 :                 });
      34              :                 break;
      35            1 :         }
      36            1 :     }
      37            1 :     return columns;
      38            1 : }
      39              : 
      40            1 : export function buildColumnSearchSQL(columns: SearchableResultColumn[], pattern: string): string | null {
      41            1 :     if (columns.length === 0) {
      42            1 :         return null;
      43              :     }
      44            1 :     const values = columns.map(column =>
      45            1 :         `(${column.columnIdx}, ${column.columnGroupIdx}, ${quoteStringLiteral(column.fieldName)})`
      46              :     );
      47            1 :     return (
      48            1 :         `SELECT column_idx, column_group_idx\n` +
      49              :         `FROM (\n` +
      50              :         `    VALUES\n` +
      51            1 :         `        ${values.join(',\n        ')}\n` +
      52              :         `) AS columns(column_idx, column_group_idx, name)\n` +
      53            1 :         `WHERE name ILIKE ${quoteStringLiteral(`%${pattern}%`)}\n` +
      54              :         `ORDER BY column_idx`
      55              :     );
      56            1 : }
      57              : 
      58            1 : export function buildDataSearchSQL(
      59            1 :     tableName: string,
      60            1 :     rowNumberFieldName: string,
      61            1 :     columns: SearchableResultColumn[],
      62            1 :     pattern: string,
      63            1 : ): string {
      64            1 :     if (columns.length === 0) {
      65            1 :         const quotedRowNumber = quoteIdent(rowNumberFieldName);
      66            1 :         return (
      67            1 :             `SELECT\n` +
      68            1 :             `    ${quotedRowNumber} AS row_idx,\n` +
      69              :             `    array_agg(0 ORDER BY 0) AS column_indices\n` +
      70            1 :             `FROM ${quoteIdent(tableName)}\n` +
      71              :             `WHERE FALSE\n` +
      72            1 :             `GROUP BY ${quotedRowNumber}\n` +
      73              :             `ORDER BY row_idx`
      74              :         );
      75            1 :     }
      76            1 :     const quotedPattern = quoteStringLiteral(`%${pattern}%`);
      77            1 :     const quotedRowNumber = quoteIdent(rowNumberFieldName);
      78            1 :     const quotedTable = quoteIdent(tableName);
      79            1 :     const unions = columns.map(column => (
      80            1 :         `    SELECT ${quotedRowNumber} AS row_idx, ${column.columnIdx} AS column_idx\n` +
      81            1 :         `    FROM ${quotedTable}\n` +
      82            1 :         `    WHERE CAST(${quoteIdent(column.fieldName)} AS TEXT) ILIKE ${quotedPattern}`
      83              :     ));
      84            1 :     return (
      85            1 :         `WITH search AS (\n` +
      86            1 :         `${unions.join('\n    UNION ALL\n')}\n` +
      87              :         `)\n` +
      88              :         `SELECT\n` +
      89              :         `    row_idx,\n` +
      90              :         `    array_agg(column_idx ORDER BY column_idx) AS column_indices\n` +
      91              :         `FROM search\n` +
      92              :         `GROUP BY row_idx\n` +
      93              :         `ORDER BY row_idx`
      94              :     );
      95            1 : }
      96              : 
      97            1 : export function parseColumnSearchMatches(table: arrow.Table): number[] {
      98            1 :     const groupColumn = table.getChild('column_group_idx') ?? table.getChildAt(1);
      99            1 :     if (groupColumn == null) {
     100            0 :         return [];
     101              :     }
     102            1 :     const matches: number[] = [];
     103            1 :     for (let i = 0; i < groupColumn.length; ++i) {
     104            1 :         matches.push(Number(groupColumn.get(i)));
     105              :     }
     106            1 :     return matches;
     107            1 : }
     108              : 
     109            1 : export function parseDataSearchMatches(table: arrow.Table): Map<number, number[]> {
     110            1 :     const rowColumn = table.getChild(DATA_SEARCH_ROW_FIELD) ?? table.getChildAt(0);
     111            1 :     const matchColumn = table.getChild('column_indices') ?? table.getChildAt(1);
     112            1 :     const matches = new Map<number, number[]>();
     113            1 :     if (rowColumn == null || matchColumn == null) {
     114            0 :         return matches;
     115              :     }
     116            1 :     for (let i = 0; i < rowColumn.length; ++i) {
     117            1 :         const rowIdx = Number(rowColumn.get(i));
     118            1 :         const rawMatches = matchColumn.get(i);
     119            1 :         const columnIndices = rawMatches == null
     120            0 :             ? []
     121            1 :             : Array.from(rawMatches as Iterable<unknown>, value => Number(value));
     122            1 :         matches.set(rowIdx, columnIndices);
     123            1 :     }
     124            1 :     return matches;
     125            1 : }
        

Generated by: LCOV version 2.0-1