import { mkdtemp, rm } from 'node:fs/promises'; import { writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import Database from 'better-sqlite3'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { createSqliteQueryExecutor, sqliteDatabasePathFromConnection } from '../../../src/context/connections/sqlite-query-executor.js'; describe('createSqliteQueryExecutor', () => { let tempDir: string; let dbPath: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), 'ktx-sqlite-query-')); dbPath = join(tempDir, 'warehouse.db'); const db = new Database(dbPath); db.exec(` CREATE TABLE orders ( id INTEGER PRIMARY KEY, status TEXT NOT NULL, amount INTEGER NOT NULL ); INSERT INTO orders (status, amount) VALUES ('paid', 20), ('paid', 30), ('open', 10); `); db.close(); }); afterEach(async () => { await rm(tempDir, { recursive: true, force: true }); }); it('executes read-only SELECT SQL against a relative SQLite path', async () => { const executor = createSqliteQueryExecutor(); const result = await executor.execute({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'sqlite', path: 'warehouse.db' }, sql: 'select status, count(*) as order_count from orders group by status order by status', maxRows: 10, }); expect(result).toEqual({ headers: ['status', 'order_count'], rows: [ ['open', 1], ['paid', 2], ], totalRows: 2, command: 'SELECT', rowCount: 2, }); }); it('supports file urls for SQLite database paths', async () => { expect( sqliteDatabasePathFromConnection({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'sqlite', url: `file://${dbPath}` }, sql: 'select 1', }), ).toBe(dbPath); }); it('resolves file references for SQLite path fields', async () => { const pointerPath = join(tempDir, 'sqlite-path.txt'); writeFileSync(pointerPath, dbPath, 'utf-8'); expect( sqliteDatabasePathFromConnection({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'sqlite', path: `file:${pointerPath}` }, sql: 'select 1', }), ).toBe(dbPath); }); it('resolves env references for SQLite database urls', async () => { const originalDatabaseUrl = process.env.KTX_SQLITE_TEST_URL; process.env.KTX_SQLITE_TEST_URL = `sqlite:${dbPath}`; try { expect( sqliteDatabasePathFromConnection({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'sqlite', url: 'env:KTX_SQLITE_TEST_URL' }, sql: 'select 1', }), ).toBe(dbPath); } finally { if (originalDatabaseUrl === undefined) { delete process.env.KTX_SQLITE_TEST_URL; } else { process.env.KTX_SQLITE_TEST_URL = originalDatabaseUrl; } } }); it('rejects mutating SQL before opening the database', async () => { const executor = createSqliteQueryExecutor(); await expect( executor.execute({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'sqlite', path: 'warehouse.db' }, sql: 'delete from orders', }), ).rejects.toThrow('Only read-only SELECT/WITH queries can be executed locally'); }); it('requires a SQLite driver and a database path', async () => { const executor = createSqliteQueryExecutor(); await expect( executor.execute({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'postgres', path: 'warehouse.db' }, sql: 'select 1', }), ).rejects.toThrow('Local SQLite execution cannot run driver "postgres"'); await expect( executor.execute({ connectionId: 'warehouse', projectDir: tempDir, connection: { driver: 'sqlite' }, sql: 'select 1', }), ).rejects.toThrow('Local SQLite execution requires connections.warehouse.path or connections.warehouse.url'); }); });