mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
|
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||
|
|
import { tmpdir } from 'node:os';
|
||
|
|
import { join } from 'node:path';
|
||
|
|
import Database from 'better-sqlite3';
|
||
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||
|
|
import { parseKtxProjectConfig, serializeKtxProjectConfig } from '../src/context/project/config.js';
|
||
|
|
import { initKtxProject } from '../src/context/project/project.js';
|
||
|
|
import type { SqlAnalysisPort } from '../src/context/sql-analysis/ports.js';
|
||
|
|
import type { KtxCliIo } from '../src/cli-runtime.js';
|
||
|
|
import { runKtxSql } from '../src/sql.js';
|
||
|
|
|
||
|
|
function fakeIo(): { io: KtxCliIo; out: () => string; err: () => string } {
|
||
|
|
let out = '';
|
||
|
|
let err = '';
|
||
|
|
return {
|
||
|
|
io: {
|
||
|
|
stdout: { write: (chunk: string) => ((out += chunk), true) },
|
||
|
|
stderr: { write: (chunk: string) => ((err += chunk), true) },
|
||
|
|
} as unknown as KtxCliIo,
|
||
|
|
out: () => out,
|
||
|
|
err: () => err,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validation needs the Python daemon, unavailable in unit tests; execution is real.
|
||
|
|
const stubSqlAnalysis: SqlAnalysisPort = {
|
||
|
|
analyzeForFingerprint: async () => ({ fingerprint: '', normalizedSql: '', tablesTouched: [], literalSlots: [] }),
|
||
|
|
analyzeBatch: async () => new Map([['cli-sql', { tablesTouched: [], columnsByClause: {} }]]),
|
||
|
|
validateReadOnly: async () => ({ ok: true, error: null }),
|
||
|
|
};
|
||
|
|
|
||
|
|
describe('ktx sql federated integration', () => {
|
||
|
|
let dir: string;
|
||
|
|
|
||
|
|
beforeEach(async () => {
|
||
|
|
dir = await mkdtemp(join(tmpdir(), 'ktx-fed-int-'));
|
||
|
|
});
|
||
|
|
|
||
|
|
afterEach(async () => {
|
||
|
|
await rm(dir, { recursive: true, force: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('joins books and reviews across two sqlite files', async () => {
|
||
|
|
const projectDir = join(dir, 'project');
|
||
|
|
await initKtxProject({ projectDir });
|
||
|
|
|
||
|
|
const books = new Database(join(projectDir, 'books.db'));
|
||
|
|
books.exec("CREATE TABLE books (id INTEGER PRIMARY KEY, title TEXT); INSERT INTO books VALUES (1, 'Clean Code');");
|
||
|
|
books.close();
|
||
|
|
const reviews = new Database(join(projectDir, 'reviews.db'));
|
||
|
|
reviews.exec('CREATE TABLE reviews (id INTEGER PRIMARY KEY, book_id INTEGER, rating INTEGER); INSERT INTO reviews VALUES (1, 1, 5);');
|
||
|
|
reviews.close();
|
||
|
|
|
||
|
|
const config = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
|
||
|
|
await writeFile(
|
||
|
|
join(projectDir, 'ktx.yaml'),
|
||
|
|
serializeKtxProjectConfig({
|
||
|
|
...config,
|
||
|
|
connections: {
|
||
|
|
books_db: { driver: 'sqlite', path: 'books.db' },
|
||
|
|
reviews_db: { driver: 'sqlite', path: 'reviews.db' },
|
||
|
|
},
|
||
|
|
}),
|
||
|
|
'utf-8',
|
||
|
|
);
|
||
|
|
|
||
|
|
const { io, out, err } = fakeIo();
|
||
|
|
const code = await runKtxSql(
|
||
|
|
{
|
||
|
|
command: 'execute',
|
||
|
|
projectDir,
|
||
|
|
connectionId: '_ktx_federated',
|
||
|
|
sql: 'SELECT b.title, r.rating FROM books_db.books b JOIN reviews_db.reviews r ON b.id = r.book_id',
|
||
|
|
maxRows: 100,
|
||
|
|
json: true,
|
||
|
|
cliVersion: 'test',
|
||
|
|
},
|
||
|
|
io,
|
||
|
|
{ createSqlAnalysis: () => stubSqlAnalysis },
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(code, err()).toBe(0);
|
||
|
|
const payload = JSON.parse(out()) as { connectionId: string; headers: string[]; rows: unknown[][] };
|
||
|
|
expect(payload.connectionId).toBe('_ktx_federated');
|
||
|
|
expect(payload.headers).toEqual(['title', 'rating']);
|
||
|
|
expect(payload.rows).toHaveLength(1);
|
||
|
|
expect(payload.rows[0][0]).toBe('Clean Code');
|
||
|
|
expect(Number(payload.rows[0][1])).toBe(5);
|
||
|
|
});
|
||
|
|
});
|