LCOV - code coverage report
Current view: top level - dashql-app/src/agent - agent_result_format.ts Coverage Total Hit
Test: DashQL 915828b Lines: 89.8 % 49 44
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import type * as arrow from 'apache-arrow';
       2              : 
       3              : /// The rows that `run` prints by default
       4              : export const DEFAULT_RUN_ROWS = 20;
       5              : /// The rows of a page of `result`
       6              : export const RESULT_PAGE_ROWS = 50;
       7              : /// A value is cut after this many characters
       8            1 : const MAX_VALUE_CHARACTERS = 200;
       9              : 
      10              : /// A column of a result
      11              : export interface AgentResultColumn {
      12              :     name: string;
      13              :     type: string;
      14              : }
      15              : 
      16              : /// A slice of a result, the structured output of `run` and `result`
      17              : export interface AgentResultSlice {
      18              :     columns: AgentResultColumn[];
      19              :     /// The rows of the slice, values as JSON
      20              :     rows: unknown[][];
      21              :     /// The total number of rows
      22              :     rowCount: number;
      23              :     /// The first row of the slice, zero-based
      24              :     offset: number;
      25              : }
      26              : 
      27              : /// A value as JSON
      28            1 : function jsonValue(value: unknown): unknown {
      29            1 :     if (value == null) return null;
      30            1 :     if (typeof value === 'bigint') {
      31            1 :         return value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : value.toString();
      32              :     }
      33            1 :     if (value instanceof Date) return value.toISOString();
      34            1 :     if (value instanceof Uint8Array) return `<${value.length} bytes>`;
      35            1 :     if (typeof value === 'object') {
      36            0 :         const json = (value as { toJSON?: () => unknown }).toJSON;
      37            0 :         if (typeof json === 'function') return jsonValue(json.call(value));
      38            0 :         if (Array.isArray(value)) return value.map(jsonValue);
      39            0 :         return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, jsonValue(v)]));
      40            0 :     }
      41            1 :     return value;
      42            1 : }
      43              : 
      44              : /// A value as text in a tab-separated row
      45            1 : function textValue(value: unknown): string {
      46            1 :     if (value == null) return 'NULL';
      47            1 :     let text = typeof value === 'string' ? value : JSON.stringify(value);
      48            1 :     text = text.replace(/[\t\r\n]/g, ' ');
      49            1 :     return text.length > MAX_VALUE_CHARACTERS ? `${text.slice(0, MAX_VALUE_CHARACTERS - 1)}…` : text;
      50            1 : }
      51              : 
      52              : /// Read rows of a result
      53            1 : export function sliceResult(table: arrow.Table, offset: number, limit: number): AgentResultSlice {
      54            1 :     const columns = table.schema.fields.map(field => ({ name: field.name, type: String(field.type) }));
      55            1 :     const rows: unknown[][] = [];
      56            1 :     const end = Math.min(table.numRows, offset + limit);
      57            1 :     for (let row = offset; row < end; ++row) {
      58            1 :         const values: unknown[] = [];
      59            1 :         for (let col = 0; col < table.numCols; ++col) {
      60            1 :             values.push(jsonValue(table.getChildAt(col)?.get(row)));
      61              :         }
      62            1 :         rows.push(values);
      63            1 :     }
      64            1 :     return { columns, rows, rowCount: table.numRows, offset };
      65            1 : }
      66              : 
      67              : /// Render a slice of a result as tab-separated text with a summary line
      68            1 : export function formatResultSlice(slice: AgentResultSlice, scriptName: string): string {
      69            1 :     const plural = (n: number, word: string) => `${n} ${word}${n === 1 ? '' : 's'}`;
      70            1 :     const lines = [`${plural(slice.rowCount, 'row')}, ${plural(slice.columns.length, 'column')}`];
      71            1 :     if (slice.columns.length > 0) {
      72            1 :         lines.push(slice.columns.map(c => `${c.name} (${c.type})`).join('\t'));
      73            1 :         for (const row of slice.rows) lines.push(row.map(textValue).join('\t'));
      74            1 :     }
      75            1 :     const shownEnd = slice.offset + slice.rows.length;
      76            1 :     if (slice.rows.length > 0 && (slice.offset > 0 || shownEnd < slice.rowCount)) {
      77            1 :         lines.push(`(rows ${slice.offset + 1}-${shownEnd} of ${slice.rowCount})`);
      78              :     }
      79            1 :     if (shownEnd < slice.rowCount) {
      80            1 :         const nextPage = Math.floor(shownEnd / RESULT_PAGE_ROWS) + 1;
      81            1 :         lines.push(`More rows: dashql agent result ${scriptName} --page ${nextPage}`);
      82            1 :     }
      83            1 :     return lines.join('\n');
      84            1 : }
        

Generated by: LCOV version 2.0-1