mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
feat(sqlite): implement connector scope listing
This commit is contained in:
parent
8ec2acba40
commit
54dd9cc518
11 changed files with 82 additions and 1 deletions
|
|
@ -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<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async listTables(_schemas?: string[]): Promise<KtxTableListEntry[]> {
|
||||
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<KtxTableSampleResult> {
|
||||
this.assertConnection(input.connectionId);
|
||||
const result = this.query(this.dialect.generateSampleQuery(this.qTableName(input.table), input.limit, input.columns));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 () => {}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]],
|
||||
|
|
|
|||
|
|
@ -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<void> } = {}): 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<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
async listTables() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async executeReadOnly(input: KtxReadOnlyQueryInput): Promise<KtxQueryResult> {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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<string, unknown>[];
|
||||
const headers = Object.keys(rows[0] ?? {});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ function connector(overrides: Partial<KtxScanConnector> = {}): KtxScanConnector
|
|||
})),
|
||||
cleanup: vi.fn(async () => {}),
|
||||
...overrides,
|
||||
listSchemas: overrides.listSchemas ?? vi.fn(async () => []),
|
||||
listTables: overrides.listTables ?? vi.fn(async () => []),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ function makeConnector(overrides: Partial<KtxScanConnector> = {}): KtxScanConnec
|
|||
})),
|
||||
cleanup: vi.fn(async () => undefined),
|
||||
...overrides,
|
||||
listSchemas: overrides.listSchemas ?? vi.fn(async () => []),
|
||||
listTables: overrides.listTables ?? vi.fn(async () => []),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue