mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
* feat(cli): define full warehouse dialect contract
* test(cli): keep dialect edge tests focused
* fix(cli): stabilize dialect contract foundation
* refactor(connectors): own read-only query preparation
* refactor(connectors): resolve dialects through registry
* refactor(connectors): keep concrete dialect classes internal
* chore(workspace): enforce dialect import boundary
* refactor(cli): resolve relationship dialect at scan boundary
* refactor(cli): use dialect display parsing for entity details
* refactor(cli): use dialect display parsing for warehouse catalog
* refactor(cli): use dialect SQL in relationship workflows
* test(cli): verify solid dialect scan workflow closure
* test: split cli tests from source tree
* refactor(cli): standardize BigQuery scope listing
* feat(sqlite): implement connector scope listing
* test(connectors): cover required table listing
* feat(cli): add warehouse driver registry
* refactor(setup): route scope discovery through driver registry
* refactor(cli): route local query execution through driver registry
* refactor(historic-sql): route dialect support through driver registry
* refactor(cli): test warehouse connections through driver registry
* fix(cli): close driver registry type export gaps
* Improve setup daemon diagnostics
* refactor(setup): centralize rail-prefixed diagnostics + query-history fallback
Extract errorMessage, writePrefixedLines, and flushPrefixedBufferedCommandOutput
into clack.ts so the setup wizard, managed daemons, and embedding/agent steps
share one rail-formatted writer. setup-databases.ts also adds a
"disable query history and retry" option when the schema-context build fails
and query history is the likely culprit, surfaced via a new
failed-query-history-unavailable status.
* fix(cli): carry catalog through the picker so BigQuery/Snowflake/SQL Server scope filters match
The setup picker's KtxTableListEntry was a 2-level { schema, name }, so
qualifiedTableId always wrote db.name into enabled_tables. When BigQuery,
Snowflake, or SQL Server later ran fast ingest, their introspect step filtered
the scope set with scopedTableNames(scope, { catalog: projectId|database, db })
— catalog was non-null on the introspect side but null in the scope refs, so
every entry was rejected, the live-database adapter staged zero table files,
and detect() failed with 'Adapter "live-database" did not recognize fetched
source output'.
Align the picker boundary with the canonical 3-level KtxTableRef:
- Add catalog: string | null to KtxTableListEntry.
- BigQuery/Snowflake/SQL Server listTables populate catalog from the
resolved projectId / database; Postgres/MySQL/ClickHouse/SQLite set null.
- qualifiedTableId emits catalog.schema.name when catalog is non-null
(resolveEnabledTables already accepts the 3-part shape) and
schemasFromEnabledTables now goes through parseDottedTableEntry so it
recovers the schema correctly from both 2-part and 3-part entries.
- Export parseDottedTableEntry from enabled-tables.ts (@internal) for picker
reuse.
Update listTables expectations in all seven connector tests and the setup /
picker test fixtures. Add a picker regression test that covers the
catalog-bearing round-trip (save + refine).
* fix(cli): allow debug telemetry under opt-out env
262 lines
8.1 KiB
TypeScript
262 lines
8.1 KiB
TypeScript
import { once } from 'node:events';
|
|
import { createServer } from 'node:http';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
import { tableRefSet } from '../../../../../src/context/scan/table-ref.js';
|
|
import { createDaemonLiveDatabaseIntrospection } from '../../../../../src/context/ingest/adapters/live-database/daemon-introspection.js';
|
|
|
|
const daemonResponse = {
|
|
connection_id: 'warehouse',
|
|
extracted_at: '2026-04-28T10:00:00+00:00',
|
|
metadata: { driver: 'postgres', schemas: ['public'] },
|
|
tables: [
|
|
{
|
|
catalog: 'warehouse',
|
|
db: 'public',
|
|
name: 'customers',
|
|
comment: null,
|
|
columns: [{ name: 'id', type: 'integer', nullable: false, primary_key: true, comment: null }],
|
|
foreign_keys: [],
|
|
},
|
|
{
|
|
catalog: 'warehouse',
|
|
db: 'public',
|
|
name: 'orders',
|
|
comment: 'Order facts',
|
|
columns: [
|
|
{ name: 'id', type: 'integer', nullable: false, primary_key: true, comment: 'Order id' },
|
|
{ name: 'customer_id', type: 'integer', nullable: false, primary_key: false, comment: null },
|
|
],
|
|
foreign_keys: [
|
|
{
|
|
from_column: 'customer_id',
|
|
to_table: 'customers',
|
|
to_column: 'id',
|
|
constraint_name: 'orders_customer_id_fkey',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
describe('createDaemonLiveDatabaseIntrospection', () => {
|
|
it('calls the database-introspect daemon command and maps the snapshot response', async () => {
|
|
const runJson = vi.fn(async () => daemonResponse);
|
|
const introspection = createDaemonLiveDatabaseIntrospection({
|
|
connections: {
|
|
warehouse: {
|
|
driver: 'postgres',
|
|
url: 'postgres://localhost:5432/warehouse',
|
|
},
|
|
},
|
|
schemas: ['public'],
|
|
runJson,
|
|
});
|
|
|
|
await expect(introspection.extractSchema('warehouse')).resolves.toEqual({
|
|
connectionId: 'warehouse',
|
|
driver: 'postgres',
|
|
extractedAt: '2026-04-28T10:00:00+00:00',
|
|
scope: { schemas: ['public'] },
|
|
metadata: { driver: 'postgres', schemas: ['public'] },
|
|
tables: [
|
|
{
|
|
catalog: 'warehouse',
|
|
db: 'public',
|
|
name: 'customers',
|
|
kind: 'table',
|
|
comment: null,
|
|
estimatedRows: null,
|
|
columns: [
|
|
{
|
|
name: 'id',
|
|
nativeType: 'integer',
|
|
normalizedType: 'integer',
|
|
dimensionType: 'number',
|
|
nullable: false,
|
|
primaryKey: true,
|
|
comment: null,
|
|
},
|
|
],
|
|
foreignKeys: [],
|
|
},
|
|
{
|
|
catalog: 'warehouse',
|
|
db: 'public',
|
|
name: 'orders',
|
|
kind: 'table',
|
|
comment: 'Order facts',
|
|
estimatedRows: null,
|
|
columns: [
|
|
{
|
|
name: 'id',
|
|
nativeType: 'integer',
|
|
normalizedType: 'integer',
|
|
dimensionType: 'number',
|
|
nullable: false,
|
|
primaryKey: true,
|
|
comment: 'Order id',
|
|
},
|
|
{
|
|
name: 'customer_id',
|
|
nativeType: 'integer',
|
|
normalizedType: 'integer',
|
|
dimensionType: 'number',
|
|
nullable: false,
|
|
primaryKey: false,
|
|
comment: null,
|
|
},
|
|
],
|
|
foreignKeys: [
|
|
{
|
|
fromColumn: 'customer_id',
|
|
toCatalog: null,
|
|
toDb: null,
|
|
toTable: 'customers',
|
|
toColumn: 'id',
|
|
constraintName: 'orders_customer_id_fkey',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(runJson).toHaveBeenCalledWith('database-introspect', {
|
|
connection_id: 'warehouse',
|
|
driver: 'postgres',
|
|
url: 'postgres://localhost:5432/warehouse',
|
|
schemas: ['public'],
|
|
statement_timeout_ms: 30_000,
|
|
connection_timeout_seconds: 5,
|
|
});
|
|
});
|
|
|
|
it('calls a running daemon HTTP endpoint when baseUrl is configured', async () => {
|
|
const requests: Array<{ url: string | undefined; body: unknown }> = [];
|
|
const server = createServer((request, response) => {
|
|
const chunks: Buffer[] = [];
|
|
request.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
request.on('end', () => {
|
|
requests.push({
|
|
url: request.url,
|
|
body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
|
|
});
|
|
response.writeHead(200, { 'content-type': 'application/json' });
|
|
response.end(JSON.stringify(daemonResponse));
|
|
});
|
|
});
|
|
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
try {
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
throw new Error('expected TCP server address');
|
|
}
|
|
const introspection = createDaemonLiveDatabaseIntrospection({
|
|
connections: {
|
|
warehouse: {
|
|
driver: 'postgres',
|
|
url: 'postgres://localhost:5432/warehouse',
|
|
},
|
|
},
|
|
baseUrl: `http://127.0.0.1:${address.port}`,
|
|
});
|
|
|
|
await expect(
|
|
introspection.extractSchema('warehouse', {
|
|
tableScope: tableRefSet([{ catalog: 'warehouse', db: 'public', name: 'orders' }]),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
connectionId: 'warehouse',
|
|
tables: [{ name: 'customers' }, { name: 'orders' }],
|
|
});
|
|
|
|
expect(requests).toEqual([
|
|
{
|
|
url: '/database/introspect',
|
|
body: {
|
|
connection_id: 'warehouse',
|
|
driver: 'postgres',
|
|
url: 'postgres://localhost:5432/warehouse',
|
|
schemas: ['public'],
|
|
statement_timeout_ms: 30_000,
|
|
connection_timeout_seconds: 5,
|
|
table_scope: [{ catalog: 'warehouse', db: 'public', name: 'orders' }],
|
|
},
|
|
},
|
|
]);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
});
|
|
|
|
it('requires a configured postgres connection with a url', async () => {
|
|
const introspection = createDaemonLiveDatabaseIntrospection({
|
|
connections: {
|
|
warehouse: {
|
|
driver: 'postgres',
|
|
},
|
|
},
|
|
runJson: vi.fn(async () => daemonResponse),
|
|
});
|
|
|
|
await expect(introspection.extractSchema('warehouse')).rejects.toThrow(
|
|
'Local live-database ingest requires connections.warehouse.url.',
|
|
);
|
|
});
|
|
|
|
it('rejects unsupported local connection drivers before calling the daemon', async () => {
|
|
const runJson = vi.fn(async () => daemonResponse);
|
|
const introspection = createDaemonLiveDatabaseIntrospection({
|
|
connections: {
|
|
warehouse: {
|
|
driver: 'snowflake',
|
|
url: 'snowflake://example',
|
|
},
|
|
},
|
|
runJson,
|
|
});
|
|
|
|
await expect(introspection.extractSchema('warehouse')).rejects.toThrow(
|
|
'Local live-database ingest cannot run driver "snowflake".',
|
|
);
|
|
expect(runJson).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not use connection enabled_tables as a response filter', async () => {
|
|
const runJson = vi.fn(async () => daemonResponse);
|
|
const introspection = createDaemonLiveDatabaseIntrospection({
|
|
connections: {
|
|
warehouse: {
|
|
driver: 'postgres',
|
|
url: 'postgres://localhost:5432/warehouse',
|
|
enabled_tables: ['public.orders'],
|
|
},
|
|
},
|
|
schemas: ['public'],
|
|
runJson,
|
|
});
|
|
|
|
const snapshot = await introspection.extractSchema('warehouse');
|
|
expect(snapshot.tables.map((table) => `${table.db}.${table.name}`)).toEqual(['public.customers', 'public.orders']);
|
|
expect(runJson).toHaveBeenCalledWith('database-introspect', expect.not.objectContaining({ table_scope: expect.anything() }));
|
|
});
|
|
|
|
it('passes through every table when enabled_tables is omitted or empty', async () => {
|
|
const runJson = vi.fn(async () => daemonResponse);
|
|
const introspection = createDaemonLiveDatabaseIntrospection({
|
|
connections: {
|
|
warehouse: {
|
|
driver: 'postgres',
|
|
url: 'postgres://localhost:5432/warehouse',
|
|
enabled_tables: [],
|
|
},
|
|
},
|
|
schemas: ['public'],
|
|
runJson,
|
|
});
|
|
|
|
const snapshot = await introspection.extractSchema('warehouse');
|
|
expect(snapshot.tables.map((table) => table.name)).toEqual(['customers', 'orders']);
|
|
});
|
|
});
|