mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-22 08:38:08 +02:00
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).
This commit is contained in:
parent
c526601d52
commit
cbe6a5f4b3
19 changed files with 193 additions and 51 deletions
|
|
@ -52,10 +52,10 @@ function captureRenderer(): {
|
|||
}
|
||||
|
||||
const discovered = [
|
||||
{ schema: 'analytics', name: 'customers', kind: 'table' as const },
|
||||
{ schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
{ schema: 'public', name: 'events', kind: 'view' as const },
|
||||
{ schema: 'public', name: 'sessions', kind: 'table' as const },
|
||||
{ catalog: null, schema: 'analytics', name: 'customers', kind: 'table' as const },
|
||||
{ catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
{ catalog: null, schema: 'public', name: 'events', kind: 'view' as const },
|
||||
{ catalog: null, schema: 'public', name: 'sessions', kind: 'table' as const },
|
||||
];
|
||||
|
||||
function promptAdapter(overrides: Partial<DatabaseScopePromptAdapter> = {}): DatabaseScopePromptAdapter {
|
||||
|
|
@ -88,7 +88,7 @@ describe('pickDatabaseScope', () => {
|
|||
select: vi.fn(async () => 'save'),
|
||||
});
|
||||
const listTablesForSchemas = vi.fn(async () => [
|
||||
{ schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
{ catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
]);
|
||||
|
||||
const result = await pickDatabaseScope(
|
||||
|
|
@ -114,6 +114,58 @@ describe('pickDatabaseScope', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('emits fully-qualified catalog.schema.name ids for catalog-bearing drivers and round-trips existing selection', async () => {
|
||||
const promptsSave = promptAdapter({
|
||||
autocompleteMultiselect: vi.fn(async () => ['analytics']),
|
||||
select: vi.fn(async () => 'save'),
|
||||
});
|
||||
const listTablesForSchemas = vi.fn(async () => [
|
||||
{ catalog: 'project-1', schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
{ catalog: 'project-1', schema: 'analytics', name: 'customers', kind: 'table' as const },
|
||||
]);
|
||||
const saveResult = await pickDatabaseScope(
|
||||
baseArgs({
|
||||
schemas: ['analytics'],
|
||||
schemaSuggestion: { excluded: new Set(), suggested: new Set(['analytics']) },
|
||||
listTablesForSchemas,
|
||||
prompts: promptsSave,
|
||||
}),
|
||||
makeIo().io,
|
||||
captureRenderer().renderer,
|
||||
);
|
||||
expect(saveResult).toEqual({
|
||||
kind: 'selected',
|
||||
activeSchemas: ['analytics'],
|
||||
enabledTables: ['project-1.analytics.orders', 'project-1.analytics.customers'],
|
||||
});
|
||||
|
||||
const { renderer, capture, setResult } = captureRenderer();
|
||||
setResult({
|
||||
kind: 'save',
|
||||
selectedIds: ['project-1.analytics.orders'],
|
||||
});
|
||||
const refineResult = await pickDatabaseScope(
|
||||
baseArgs({
|
||||
schemas: ['analytics'],
|
||||
schemaSuggestion: { excluded: new Set(), suggested: new Set(['analytics']) },
|
||||
existing: { enabledTables: ['project-1.analytics.orders'] },
|
||||
listTablesForSchemas,
|
||||
prompts: promptAdapter({
|
||||
autocompleteMultiselect: vi.fn(async () => ['analytics']),
|
||||
select: vi.fn(async () => 'refine'),
|
||||
}),
|
||||
}),
|
||||
makeIo().io,
|
||||
renderer,
|
||||
);
|
||||
expect(refineResult).toEqual({
|
||||
kind: 'selected',
|
||||
activeSchemas: ['analytics'],
|
||||
enabledTables: ['project-1.analytics.orders'],
|
||||
});
|
||||
expect([...(capture.state?.checked ?? [])]).toContain('project-1.analytics.orders');
|
||||
});
|
||||
|
||||
it('routes partial existing allowlists through Stage 2 so save preserves table selections', async () => {
|
||||
const { renderer, setResult } = captureRenderer();
|
||||
setResult({ kind: 'save', selectedIds: ['analytics.customers'] });
|
||||
|
|
@ -122,8 +174,8 @@ describe('pickDatabaseScope', () => {
|
|||
select: vi.fn(async () => 'save'),
|
||||
});
|
||||
const listTablesForSchemas = vi.fn(async () => [
|
||||
{ schema: 'analytics', name: 'customers', kind: 'table' as const },
|
||||
{ schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
{ catalog: null, schema: 'analytics', name: 'customers', kind: 'table' as const },
|
||||
{ catalog: null, schema: 'analytics', name: 'orders', kind: 'table' as const },
|
||||
]);
|
||||
|
||||
const result = await pickDatabaseScope(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue