mirror of
https://github.com/Kaelio/ktx.git
synced 2026-07-25 12:01:03 +02:00
refactor(io): extract shared result-table formatters from sql
Move formatValue/printJson/printPlain/printPretty/printResultTable and the KtxResultTable interface out of sql.ts into io/result-table.ts so the upcoming mongo-query command can reuse them verbatim.
This commit is contained in:
parent
9a2e215bdc
commit
89b8729950
3 changed files with 108 additions and 58 deletions
59
packages/cli/src/io/result-table.ts
Normal file
59
packages/cli/src/io/result-table.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { KtxCliIo } from '../cli-runtime.js';
|
||||
import type { KtxOutputMode } from './mode.js';
|
||||
|
||||
export interface KtxResultTable {
|
||||
connectionId: string;
|
||||
headers: string[];
|
||||
headerTypes?: string[];
|
||||
rows: unknown[][];
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function formatValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function printJson(output: KtxResultTable, io: KtxCliIo): void {
|
||||
io.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function printPlain(output: KtxResultTable, io: KtxCliIo): void {
|
||||
io.stdout.write(`${output.headers.join('\t')}\n`);
|
||||
for (const row of output.rows) {
|
||||
io.stdout.write(`${row.map(formatValue).join('\t')}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function printPretty(output: KtxResultTable, io: KtxCliIo): void {
|
||||
const rows = output.rows.map((row) => row.map(formatValue));
|
||||
const widths = output.headers.map((header, index) =>
|
||||
Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)),
|
||||
);
|
||||
const renderRow = (cells: string[]): string =>
|
||||
cells.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(' ').trimEnd();
|
||||
|
||||
if (output.headers.length > 0) {
|
||||
io.stdout.write(`${renderRow(output.headers)}\n`);
|
||||
io.stdout.write(`${renderRow(widths.map((width) => '-'.repeat(width)))}\n`);
|
||||
}
|
||||
for (const row of rows) {
|
||||
io.stdout.write(`${renderRow(row)}\n`);
|
||||
}
|
||||
io.stdout.write(`\n${output.rowCount} ${output.rowCount === 1 ? 'row' : 'rows'}\n`);
|
||||
}
|
||||
|
||||
export function printResultTable(output: KtxResultTable, mode: KtxOutputMode, io: KtxCliIo): void {
|
||||
if (mode === 'json') {
|
||||
printJson(output, io);
|
||||
return;
|
||||
}
|
||||
if (mode === 'plain') {
|
||||
printPlain(output, io);
|
||||
return;
|
||||
}
|
||||
printPretty(output, io);
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { sqlAnalysisDialectForDriver } from './context/sql-analysis/dialect.js';
|
|||
import type { SqlAnalysisDialect, SqlAnalysisPort } from './context/sql-analysis/ports.js';
|
||||
import type { KtxCliIo } from './cli-runtime.js';
|
||||
import { type KtxOutputMode, resolveOutputMode } from './io/mode.js';
|
||||
import { type KtxResultTable, printResultTable } from './io/result-table.js';
|
||||
import { createKtxCliScanConnector } from './local-scan-connectors.js';
|
||||
import { createManagedDaemonSqlAnalysisPort } from './managed-python-http.js';
|
||||
import { profileMark } from './startup-profile.js';
|
||||
|
|
@ -39,14 +40,6 @@ export interface KtxSqlDeps {
|
|||
executeFederated?: typeof executeFederatedQuery;
|
||||
}
|
||||
|
||||
interface SqlExecutionOutput {
|
||||
connectionId: string;
|
||||
headers: string[];
|
||||
headerTypes?: string[];
|
||||
rows: unknown[][];
|
||||
rowCount: number;
|
||||
}
|
||||
|
||||
function queryVerb(sql: string): 'select' | 'explain' | 'show' | 'with' | 'other' {
|
||||
const first = sql.trim().split(/\s+/, 1)[0]?.toLowerCase();
|
||||
if (first === 'select' || first === 'explain' || first === 'show' || first === 'with') {
|
||||
|
|
@ -68,55 +61,7 @@ async function safeReferencedTableCount(
|
|||
}
|
||||
}
|
||||
|
||||
function formatValue(value: unknown): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') return String(value);
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function printJson(output: SqlExecutionOutput, io: KtxCliIo): void {
|
||||
io.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function printPlain(output: SqlExecutionOutput, io: KtxCliIo): void {
|
||||
io.stdout.write(`${output.headers.join('\t')}\n`);
|
||||
for (const row of output.rows) {
|
||||
io.stdout.write(`${row.map(formatValue).join('\t')}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
function printPretty(output: SqlExecutionOutput, io: KtxCliIo): void {
|
||||
const rows = output.rows.map((row) => row.map(formatValue));
|
||||
const widths = output.headers.map((header, index) =>
|
||||
Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)),
|
||||
);
|
||||
const renderRow = (cells: string[]): string =>
|
||||
cells.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(' ').trimEnd();
|
||||
|
||||
if (output.headers.length > 0) {
|
||||
io.stdout.write(`${renderRow(output.headers)}\n`);
|
||||
io.stdout.write(`${renderRow(widths.map((width) => '-'.repeat(width)))}\n`);
|
||||
}
|
||||
for (const row of rows) {
|
||||
io.stdout.write(`${renderRow(row)}\n`);
|
||||
}
|
||||
io.stdout.write(`\n${output.rowCount} ${output.rowCount === 1 ? 'row' : 'rows'}\n`);
|
||||
}
|
||||
|
||||
function printSqlResult(output: SqlExecutionOutput, mode: KtxSqlOutputMode, io: KtxCliIo): void {
|
||||
if (mode === 'json') {
|
||||
printJson(output, io);
|
||||
return;
|
||||
}
|
||||
if (mode === 'plain') {
|
||||
printPlain(output, io);
|
||||
return;
|
||||
}
|
||||
printPretty(output, io);
|
||||
}
|
||||
|
||||
function resultOutput(connectionId: string, result: KtxSqlQueryExecutionResult): SqlExecutionOutput {
|
||||
function resultOutput(connectionId: string, result: KtxSqlQueryExecutionResult): KtxResultTable {
|
||||
return {
|
||||
connectionId,
|
||||
headers: result.headers,
|
||||
|
|
@ -168,7 +113,7 @@ export async function runKtxSql(args: KtxSqlArgs, io: KtxCliIo = process, deps:
|
|||
});
|
||||
const referencedTableCount = await safeReferencedTableCount(analysisPort, args.sql, dialect);
|
||||
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
|
||||
printSqlResult(resultOutput(args.connectionId, result), mode, io);
|
||||
printResultTable(resultOutput(args.connectionId, result), mode, io);
|
||||
await emitTelemetryEvent({
|
||||
name: 'sql_completed',
|
||||
projectDir: args.projectDir,
|
||||
|
|
|
|||
46
packages/cli/test/io/result-table.test.ts
Normal file
46
packages/cli/test/io/result-table.test.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { formatValue, printResultTable } from '../../src/io/result-table.js';
|
||||
import type { KtxCliIo } from '../../src/cli-runtime.js';
|
||||
|
||||
function captureIo(): { io: KtxCliIo; out: () => string } {
|
||||
let buffer = '';
|
||||
const io = {
|
||||
stdout: { write: (s: string) => { buffer += s; return true; } },
|
||||
stderr: { write: () => true },
|
||||
} as unknown as KtxCliIo;
|
||||
return { io, out: () => buffer };
|
||||
}
|
||||
|
||||
describe('formatValue', () => {
|
||||
it('renders null/undefined as empty, scalars as strings, objects as JSON', () => {
|
||||
expect(formatValue(null)).toBe('');
|
||||
expect(formatValue(undefined)).toBe('');
|
||||
expect(formatValue('x')).toBe('x');
|
||||
expect(formatValue(42)).toBe('42');
|
||||
expect(formatValue(true)).toBe('true');
|
||||
expect(formatValue({ a: 1 })).toBe(JSON.stringify({ a: 1 }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('printResultTable', () => {
|
||||
const table = { connectionId: 'mongo', headers: ['_id', 'city'], rows: [['a1', 'NY']], rowCount: 1 };
|
||||
|
||||
it('json mode prints the structured payload', () => {
|
||||
const { io, out } = captureIo();
|
||||
printResultTable(table, 'json', io);
|
||||
expect(JSON.parse(out())).toEqual(table);
|
||||
});
|
||||
|
||||
it('plain mode prints tab-separated headers and rows', () => {
|
||||
const { io, out } = captureIo();
|
||||
printResultTable(table, 'plain', io);
|
||||
expect(out()).toBe('_id\tcity\na1\tNY\n');
|
||||
});
|
||||
|
||||
it('pretty mode prints a header rule and a row count', () => {
|
||||
const { io, out } = captureIo();
|
||||
printResultTable(table, 'pretty', io);
|
||||
expect(out()).toContain('_id');
|
||||
expect(out()).toContain('1 row');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue