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 : }
|