diff --git a/packages/cli/src/connectors/sqlite/connector.ts b/packages/cli/src/connectors/sqlite/connector.ts index bb101cdd..13b02a0f 100644 --- a/packages/cli/src/connectors/sqlite/connector.ts +++ b/packages/cli/src/connectors/sqlite/connector.ts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; import { getDialectForDriver } from '../../context/connections/dialects.js'; import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js'; import { normalizeQueryRows } from '../../context/connections/query-executor.js'; -import { createKtxConnectorCapabilities, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaForeignKey, type KtxSchemaSnapshot, type KtxSchemaTable, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js'; +import { createKtxConnectorCapabilities, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaForeignKey, type KtxSchemaSnapshot, type KtxSchemaTable, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js'; import { scopedTableNames } from '../../context/scan/table-ref.js'; export interface KtxSqliteConnectionConfig { @@ -209,6 +209,30 @@ export class KtxSqliteScanConnector implements KtxScanConnector { }; } + async listSchemas(): Promise { + return []; + } + + async listTables(_schemas?: string[]): Promise { + const rows = this.database() + .prepare( + ` + SELECT name, type + FROM sqlite_master + WHERE type IN ('table', 'view') + AND name NOT LIKE 'sqlite_%' + ORDER BY name + `, + ) + .all() as SqliteMasterRow[]; + + return rows.map((row) => ({ + schema: '', + name: row.name, + kind: row.type === 'view' ? ('view' as const) : ('table' as const), + })); + } + async sampleTable(input: KtxTableSampleInput, _ctx: KtxScanContext): Promise { this.assertConnection(input.connectionId); const result = this.query(this.dialect.generateSampleQuery(this.qTableName(input.table), input.limit, input.columns)); diff --git a/packages/cli/test/connection.test.ts b/packages/cli/test/connection.test.ts index 0cf1f3a4..59ead362 100644 --- a/packages/cli/test/connection.test.ts +++ b/packages/cli/test/connection.test.ts @@ -59,6 +59,8 @@ function nativeConnector( introspect: vi.fn(async () => { throw new Error('introspect should not be called from connection test'); }), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), testConnection, cleanup, }; diff --git a/packages/cli/test/connectors/sqlite/connector.test.ts b/packages/cli/test/connectors/sqlite/connector.test.ts index e73f649a..4cc8cb80 100644 --- a/packages/cli/test/connectors/sqlite/connector.test.ts +++ b/packages/cli/test/connectors/sqlite/connector.test.ts @@ -150,6 +150,20 @@ describe('KtxSqliteScanConnector', () => { ]); }); + it('lists schemaless tables and views for setup discovery', async () => { + const connector = new KtxSqliteScanConnector({ + connectionId: 'warehouse', + connection: { driver: 'sqlite', path: dbPath }, + }); + + await expect(connector.listSchemas()).resolves.toEqual([]); + await expect(connector.listTables(['ignored'])).resolves.toEqual([ + { schema: '', name: 'customers', kind: 'table' }, + { schema: '', name: 'orders', kind: 'table' }, + { schema: '', name: 'recent_orders', kind: 'view' }, + ]); + }); + it('runs samples, distinct values, statistics, and read-only SQL', async () => { const connector = new KtxSqliteScanConnector({ connectionId: 'warehouse', diff --git a/packages/cli/test/context/mcp/local-project-ports.test.ts b/packages/cli/test/context/mcp/local-project-ports.test.ts index 063c73d1..afe85b4c 100644 --- a/packages/cli/test/context/mcp/local-project-ports.test.ts +++ b/packages/cli/test/context/mcp/local-project-ports.test.ts @@ -56,6 +56,8 @@ describe('createLocalProjectMcpContextPorts', () => { driver: snapshot.driver, capabilities: createKtxConnectorCapabilities({ readOnlySql: queryResult !== undefined }), introspect: vi.fn(async () => snapshot), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), executeReadOnly: queryResult === undefined ? undefined : vi.fn(async () => queryResult), cleanup: vi.fn(async () => {}), }; diff --git a/packages/cli/test/context/scan/description-generation.test.ts b/packages/cli/test/context/scan/description-generation.test.ts index 6451d441..811752e5 100644 --- a/packages/cli/test/context/scan/description-generation.test.ts +++ b/packages/cli/test/context/scan/description-generation.test.ts @@ -72,6 +72,8 @@ function createConnector(): KtxScanConnector { introspect: vi.fn(async () => { throw new Error('introspection is not used by description generation'); }), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), sampleColumn: vi.fn(async () => ({ values: ['paid', 'refunded', null], nullCount: 1, diff --git a/packages/cli/test/context/scan/local-enrichment.test.ts b/packages/cli/test/context/scan/local-enrichment.test.ts index 8743b846..9704d071 100644 --- a/packages/cli/test/context/scan/local-enrichment.test.ts +++ b/packages/cli/test/context/scan/local-enrichment.test.ts @@ -104,6 +104,8 @@ function connector(): KtxScanConnector { columnStats: true, }), introspect: vi.fn(async () => snapshot), + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), sampleTable: vi.fn(async () => ({ headers: ['id', 'customer_id'], rows: [[1, 10]], diff --git a/packages/cli/test/context/scan/local-scan.test.ts b/packages/cli/test/context/scan/local-scan.test.ts index 0548857a..931fd6b0 100644 --- a/packages/cli/test/context/scan/local-scan.test.ts +++ b/packages/cli/test/context/scan/local-scan.test.ts @@ -16,6 +16,11 @@ import type { KtxSchemaSnapshot, } from '../../../src/context/scan/types.js'; +const connectorScopeListing = { + listSchemas: vi.fn(async () => []), + listTables: vi.fn(async () => []), +}; + function relationshipSqlResult( input: KtxReadOnlyQueryInput, options: { throwOnCoverage?: boolean } = {}, @@ -254,6 +259,7 @@ function nativeScanConnector(options: { cleanup?: () => Promise } = {}): K formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, introspect: vi.fn(async () => nativeScanSnapshot()), sampleTable: vi.fn(async () => ({ headers: ['id'], rows: [[1]], totalRows: 1 })), sampleColumn: vi.fn(async () => ({ values: ['1'], nullCount: 0, distinctCount: 1 })), @@ -656,6 +662,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -741,6 +748,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -930,6 +938,14 @@ describe('local scan', () => { }; } + async listSchemas(): Promise { + return []; + } + + async listTables() { + return []; + } + async executeReadOnly(input: KtxReadOnlyQueryInput): Promise { return relationshipSqlResult(input); } @@ -972,6 +988,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1073,6 +1090,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1200,6 +1218,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1340,6 +1359,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: true, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1455,6 +1475,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1550,6 +1571,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', @@ -1666,6 +1688,7 @@ describe('local scan', () => { formalForeignKeys: false, estimatedRowCounts: false, }, + ...connectorScopeListing, async introspect() { return { connectionId: 'warehouse', diff --git a/packages/cli/test/context/scan/relationship-discovery.test.ts b/packages/cli/test/context/scan/relationship-discovery.test.ts index d959f506..55341645 100644 --- a/packages/cli/test/context/scan/relationship-discovery.test.ts +++ b/packages/cli/test/context/scan/relationship-discovery.test.ts @@ -213,6 +213,8 @@ function connector(executor: InMemorySqliteExecutor | null): KtxScanConnector { columnSampling: false, }), introspect: async () => snapshot(), + listSchemas: async () => [], + listTables: async () => [], executeReadOnly: executor ? executor.executeReadOnly.bind(executor) : undefined, }; } @@ -645,6 +647,8 @@ describe('production relationship discovery', () => { columnSampling: false, }), introspect: async () => maskedSnapshot, + listSchemas: async () => [], + listTables: async () => [], executeReadOnly: async (input) => { const rows = database.prepare(input.sql).all() as Record[]; const headers = Object.keys(rows[0] ?? {}); diff --git a/packages/cli/test/context/scan/types.test.ts b/packages/cli/test/context/scan/types.test.ts index 93a7d412..8aa55dba 100644 --- a/packages/cli/test/context/scan/types.test.ts +++ b/packages/cli/test/context/scan/types.test.ts @@ -93,6 +93,8 @@ describe('KTX scan contract types', () => { expect(ctx.runId).toBe('scan-run-1'); return snapshot; }, + listSchemas: async () => [], + listTables: async () => [], }; await expect( @@ -164,6 +166,8 @@ describe('KTX scan contract types', () => { tables: [], }; }, + listSchemas: async () => [], + listTables: async () => [], }; await expect( diff --git a/packages/cli/test/ingest-query-executor.test.ts b/packages/cli/test/ingest-query-executor.test.ts index f7f907f2..372cd362 100644 --- a/packages/cli/test/ingest-query-executor.test.ts +++ b/packages/cli/test/ingest-query-executor.test.ts @@ -31,6 +31,8 @@ function connector(overrides: Partial = {}): KtxScanConnector })), cleanup: vi.fn(async () => {}), ...overrides, + listSchemas: overrides.listSchemas ?? vi.fn(async () => []), + listTables: overrides.listTables ?? vi.fn(async () => []), }; } diff --git a/packages/cli/test/sql.test.ts b/packages/cli/test/sql.test.ts index 62ae1a69..b48ebe5b 100644 --- a/packages/cli/test/sql.test.ts +++ b/packages/cli/test/sql.test.ts @@ -66,6 +66,8 @@ function makeConnector(overrides: Partial = {}): KtxScanConnec })), cleanup: vi.fn(async () => undefined), ...overrides, + listSchemas: overrides.listSchemas ?? vi.fn(async () => []), + listTables: overrides.listTables ?? vi.fn(async () => []), }; }