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

            Line data    Source code
       1              : import * as dashql from '@ankoh/dashql-core';
       2              : 
       3              : import { formatSqlText } from '../../session/scripts/script_format.js';
       4              : 
       5              : /// The name of the common table expression that holds the query of the script
       6            1 : const PROJECTION_INPUT = 'base';
       7              : 
       8              : /// Why a script cannot get the projection of its result
       9            1 : export enum ResultProjectionError {
      10              :     /// The script does not parse or has no statement
      11            1 :     NotParsed = 'not_parsed',
      12              :     /// The last statement of the script is no query
      13            1 :     NoQuery = 'no_query',
      14              :     /// The result has no columns
      15            1 :     NoColumns = 'no_columns',
      16              :     /// Two columns of the result have the same name, a projection cannot tell them apart
      17            1 :     DuplicateColumns = 'duplicate_columns',
      18            1 : }
      19              : 
      20              : /// Wrap the terminal query of a script, its last statement, into a common table expression and select every
      21              : /// column of its result below it: `with base as (<query>) select a, b from base`. A column is excluded by
      22              : /// deleting its line. The new statement is formatted with the pretty printer, the rest of the script stays.
      23              : /// Returns the new text of the script.
      24            1 : export function addResultProjection(
      25            1 :     core: dashql.DashQL,
      26            1 :     scriptText: string,
      27            1 :     columnNames: string[],
      28            1 :     config: dashql.buffers.formatting.FormattingConfigT,
      29            1 : ): string | ResultProjectionError {
      30            1 :     if (columnNames.length === 0) return ResultProjectionError.NoColumns;
      31            1 :     if (new Set(columnNames).size !== columnNames.length) return ResultProjectionError.DuplicateColumns;
      32              : 
      33              :     // The span of the terminal query, in UTF-8 bytes
      34            1 :     const bytes = new TextEncoder().encode(scriptText);
      35            1 :     const catalog = core.createCatalog();
      36            1 :     const script = core.createScript(catalog);
      37            1 :     let span: { offset: number; length: number } | null = null;
      38            1 :     try {
      39            1 :         script.replaceText(scriptText);
      40            1 :         script.parse();
      41            1 :         const reader = script.getParsed().read();
      42            1 :         if (reader.scannerErrorsLength() > 0 || reader.parserErrorsLength() > 0 || reader.statementsLength() === 0) {
      43            1 :             return ResultProjectionError.NotParsed;
      44              :         }
      45            1 :         const statement = reader.statements(reader.statementsLength() - 1)!;
      46            1 :         if (statement.statementType() !== dashql.buffers.parser.StatementType.SELECT) {
      47            1 :             return ResultProjectionError.NoQuery;
      48              :         }
      49            1 :         const statementSpan = statement.statementSpan();
      50            1 :         if (statementSpan == null) return ResultProjectionError.NotParsed;
      51            1 :         span = { offset: statementSpan.offset(), length: statementSpan.length() };
      52            1 :     } finally {
      53            1 :         script.destroy();
      54            1 :         catalog.destroy();
      55            1 :     }
      56              : 
      57            1 :     const decoder = new TextDecoder();
      58            1 :     const before = decoder.decode(bytes.subarray(0, span.offset));
      59            1 :     const query = decoder.decode(bytes.subarray(span.offset, span.offset + span.length));
      60            1 :     const after = decoder.decode(bytes.subarray(span.offset + span.length));
      61              : 
      62            1 :     const input = core.quoteColumnName(PROJECTION_INPUT);
      63            1 :     const columns = columnNames.map(name => core.quoteColumnName(name));
      64            1 :     const projection = `with ${input} as (${query}) select ${columns.join(', ')} from ${input};`;
      65              :     // The separator of the statement stays in the text after it
      66            1 :     const formatted = formatSqlText(core, projection, config)?.trimEnd().replace(/;$/, '')
      67            0 :         ?? layoutProjection(input, query, columns);
      68            1 :     return `${before}${formatted}${after}`;
      69            1 : }
      70              : 
      71              : /// The projection when the formatter cannot format the query, the query keeps its text
      72            0 : function layoutProjection(input: string, query: string, columns: string[]): string {
      73            0 :     return `with ${input} as (
      74            0 :     ${query.split('\n').join('\n    ')}
      75              : )
      76              : select
      77            0 :     ${columns.join(',\n    ')}
      78            0 : from ${input}`;
      79            0 : }
        

Generated by: LCOV version 2.0-1