feat: add schema-first database scope picker

This commit is contained in:
Andrey Avtomonov 2026-05-21 20:02:07 +02:00
parent 4efa061c0e
commit 8e17bf25af
4 changed files with 330 additions and 139 deletions

View file

@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest';
import {
pickDatabaseScope,
type DatabaseScopePromptAdapter,
type DatabaseTreePickerRenderer,
type PickDatabaseScopeArgs,
} from './database-tree-picker.js';
@ -12,8 +13,17 @@ function makeIo() {
let stderr = '';
return {
io: {
stdout: { isTTY: true, write: (chunk: string) => { stdout += chunk; } },
stderr: { write: (chunk: string) => { stderr += chunk; } },
stdout: {
isTTY: true,
write: (chunk: string) => {
stdout += chunk;
},
},
stderr: {
write: (chunk: string) => {
stderr += chunk;
},
},
},
stdout: () => stdout,
stderr: () => stderr,
@ -48,23 +58,96 @@ const discovered = [
{ schema: 'public', name: 'sessions', kind: 'table' as const },
];
function promptAdapter(overrides: Partial<DatabaseScopePromptAdapter> = {}): DatabaseScopePromptAdapter {
return {
autocompleteMultiselect: vi.fn(async () => ['analytics']),
select: vi.fn(async () => 'refine'),
...overrides,
};
}
function baseArgs(overrides: Partial<PickDatabaseScopeArgs> = {}): PickDatabaseScopeArgs {
return {
connectionId: 'warehouse',
schemaNoun: 'schema',
schemaNounPlural: 'schemas',
discovered,
schemas: ['analytics', 'public'],
schemaSuggestion: { excluded: new Set(), suggested: new Set(['analytics']) },
existing: { enabledTables: [] },
defaultSchemas: ['analytics'],
supportsSchemaScope: true,
listTablesForSchemas: vi.fn(async () => discovered),
prompts: promptAdapter(),
...overrides,
};
}
describe('pickDatabaseScope', () => {
it('starts Stage 1 with no checked schemas and does not enumerate tables before schema selection', async () => {
const prompts = promptAdapter({
autocompleteMultiselect: vi.fn(async () => ['analytics']),
select: vi.fn(async () => 'save'),
});
const listTablesForSchemas = vi.fn(async () => [
{ schema: 'analytics', name: 'orders', kind: 'table' as const },
]);
const result = await pickDatabaseScope(
baseArgs({
connectionId: 'warehouse',
schemaNoun: 'dataset',
schemaNounPlural: 'datasets',
schemas: ['analytics', 'raw'],
schemaSuggestion: { excluded: new Set(['raw']), suggested: new Set(['analytics']) },
listTablesForSchemas,
prompts,
}),
makeIo().io,
captureRenderer().renderer,
);
expect(listTablesForSchemas).toHaveBeenCalledTimes(1);
expect(listTablesForSchemas).toHaveBeenCalledWith(['analytics']);
expect(result).toEqual({
kind: 'selected',
activeSchemas: ['analytics'],
enabledTables: ['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'] });
const prompts = promptAdapter({
autocompleteMultiselect: vi.fn(async () => ['analytics']),
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 },
]);
const result = await pickDatabaseScope(
baseArgs({
schemas: ['analytics'],
schemaSuggestion: { excluded: new Set(), suggested: new Set(['analytics']) },
existing: { enabledTables: ['analytics.customers'] },
listTablesForSchemas,
prompts,
}),
makeIo().io,
renderer,
);
expect(result).toEqual({
kind: 'selected',
activeSchemas: ['analytics'],
enabledTables: ['analytics.customers'],
});
});
it('builds a 2-level tree (schemas as parents, tables as children) and uses save-empty action', async () => {
const { renderer, capture, setResult } = captureRenderer();
setResult({ kind: 'quit' });
setResult({ kind: 'save', selectedIds: ['analytics'] });
await pickDatabaseScope(baseArgs(), makeIo().io, renderer);
@ -81,18 +164,18 @@ describe('pickDatabaseScope', () => {
expect(capture.state?.byId.get('public.events')?.title).toBe('events (view)');
});
it('pre-checks default schemas at the parent level when no existing selection', async () => {
it('pre-checks selected schemas at the parent level when no existing selection reaches Stage 2', async () => {
const { renderer, capture, setResult } = captureRenderer();
setResult({ kind: 'quit' });
setResult({ kind: 'save', selectedIds: ['analytics'] });
await pickDatabaseScope(baseArgs({ defaultSchemas: ['analytics'] }), makeIo().io, renderer);
await pickDatabaseScope(baseArgs(), makeIo().io, renderer);
expect([...(capture.state?.checked ?? [])]).toEqual(['analytics']);
});
it('collapses an existing full-schema selection back into the parent check', async () => {
const { renderer, capture, setResult } = captureRenderer();
setResult({ kind: 'quit' });
setResult({ kind: 'save', selectedIds: ['analytics'] });
await pickDatabaseScope(
baseArgs({ existing: { enabledTables: ['analytics.customers', 'analytics.orders'] } }),
@ -105,7 +188,7 @@ describe('pickDatabaseScope', () => {
it('keeps a partial existing selection at the leaf level', async () => {
const { renderer, capture, setResult } = captureRenderer();
setResult({ kind: 'quit' });
setResult({ kind: 'save', selectedIds: ['analytics.customers'] });
await pickDatabaseScope(
baseArgs({ existing: { enabledTables: ['analytics.customers'] } }),
@ -142,24 +225,6 @@ describe('pickDatabaseScope', () => {
});
});
it('treats empty save as enable-all', async () => {
const { renderer, setResult } = captureRenderer();
setResult({ kind: 'save', selectedIds: [] });
const result = await pickDatabaseScope(baseArgs(), makeIo().io, renderer);
expect(result).toEqual({
kind: 'selected',
activeSchemas: ['analytics', 'public'],
enabledTables: [
'analytics.customers',
'analytics.orders',
'public.events',
'public.sessions',
],
});
});
it('omits activeSchemas when the driver does not support a schema scope', async () => {
const { renderer, setResult } = captureRenderer();
setResult({ kind: 'save', selectedIds: ['analytics'] });
@ -177,11 +242,12 @@ describe('pickDatabaseScope', () => {
});
});
it('returns back when the picker quits', async () => {
const { renderer, setResult } = captureRenderer();
setResult({ kind: 'quit' });
it('returns back when Stage 1 is cancelled', async () => {
const prompts = promptAdapter({
autocompleteMultiselect: vi.fn(async () => ['back']),
});
const result = await pickDatabaseScope(baseArgs(), makeIo().io, renderer);
const result = await pickDatabaseScope(baseArgs({ prompts }), makeIo().io, captureRenderer().renderer);
expect(result).toEqual({ kind: 'back' });
});

View file

@ -43,15 +43,33 @@ interface ScopeSuggestion {
suggested: Set<string>;
}
/** @internal */
export interface DatabaseScopePromptAdapter {
autocompleteMultiselect(options: {
message: string;
options: Array<{ value: string; label: string; hint?: string; disabled?: boolean }>;
placeholder?: string;
required?: boolean;
maxItems?: number;
initialValues?: string[];
}): Promise<string[]>;
select(options: {
message: string;
options: Array<{ value: string; label: string; hint?: string; disabled?: boolean }>;
}): Promise<string>;
}
export interface PickDatabaseScopeArgs {
connectionId: string;
schemaNoun: string;
schemaNounPlural: string;
discovered: readonly KtxTableListEntry[];
schemas: readonly string[];
schemaSuggestion: ScopeSuggestion;
existing: { enabledTables: readonly string[] };
defaultSchemas: readonly string[];
schemaSuggestion?: ScopeSuggestion;
supportsSchemaScope: boolean;
listTablesForSchemas: (schemas: string[]) => Promise<KtxTableListEntry[]>;
initialSchemas?: readonly string[];
prompts: DatabaseScopePromptAdapter;
}
function qualifiedTableId(entry: KtxTableListEntry): string {
@ -167,12 +185,39 @@ function schemasFromEnabledTables(enabledTables: readonly string[]): string[] {
return result;
}
export async function pickDatabaseScope(
args: PickDatabaseScopeArgs,
io: KtxCliIo,
render: DatabaseTreePickerRenderer = defaultRenderer,
): Promise<DatabaseScopePickResult> {
const { inputs, schemaIds, allTables } = buildTreeInputs(args.discovered);
function schemaOptions(args: PickDatabaseScopeArgs): Array<{ value: string; label: string; hint?: string }> {
return args.schemas
.filter((schema) => !args.schemaSuggestion.excluded.has(schema))
.slice()
.sort((left, right) => {
const leftSuggested = args.schemaSuggestion.suggested.has(left);
const rightSuggested = args.schemaSuggestion.suggested.has(right);
if (leftSuggested !== rightSuggested) return leftSuggested ? -1 : 1;
return left.localeCompare(right);
})
.map((schema) => ({
value: schema,
label: schema,
...(args.schemaSuggestion.suggested.has(schema) ? { hint: 'suggested' } : {}),
}));
}
function initialStageOneSchemas(args: PickDatabaseScopeArgs): string[] {
if (args.existing.enabledTables.length > 0) {
return schemasFromEnabledTables(args.existing.enabledTables);
}
return [...(args.initialSchemas ?? [])];
}
async function runStageTwoTreePicker(input: {
args: PickDatabaseScopeArgs;
discovered: readonly KtxTableListEntry[];
selectedSchemas: readonly string[];
io: KtxCliIo;
render: DatabaseTreePickerRenderer;
}): Promise<DatabaseScopePickResult> {
const { args, discovered, selectedSchemas, io, render } = input;
const { inputs, schemaIds, allTables } = buildTreeInputs(discovered);
const tree = buildPickerTree(inputs);
const byId = new Map(tree.map((node) => [node.id, node]));
const tableCount = allTables.length;
@ -181,7 +226,7 @@ export async function pickDatabaseScope(
const initialSelection =
args.existing.enabledTables.length > 0
? initialSelectionForExisting(args.existing.enabledTables, byId)
: initialSelectionFromDefaults(args.defaultSchemas, schemaIds);
: initialSelectionFromDefaults(selectedSchemas, schemaIds);
const initialState = buildInitialState({
tree,
@ -214,3 +259,61 @@ export async function pickDatabaseScope(
return { kind: 'selected', activeSchemas, enabledTables };
}
export async function pickDatabaseScope(
args: PickDatabaseScopeArgs,
io: KtxCliIo,
render: DatabaseTreePickerRenderer = defaultRenderer,
): Promise<DatabaseScopePickResult> {
let selectedSchemas = initialStageOneSchemas(args);
while (true) {
const pickedSchemas = await args.prompts.autocompleteMultiselect({
message: `Choose ${args.schemaNounPlural} to enable for ${args.connectionId}\nType to filter. Space to select. Enter when done.`,
placeholder: `Search ${args.schemaNounPlural}`,
options: schemaOptions(args),
initialValues: selectedSchemas,
required: false,
});
if (pickedSchemas.includes('back')) {
return { kind: 'back' };
}
selectedSchemas = pickedSchemas;
if (selectedSchemas.length === 0) {
io.stderr.write(`Nothing selected - type to filter, or Escape to skip ${args.schemaNoun} scope.\n`);
continue;
}
const action = await args.prompts.select({
message: `Save ${selectedSchemas.length} ${selectedSchemas.length === 1 ? args.schemaNoun : args.schemaNounPlural} or refine tables?`,
options: [
{ value: 'save', label: 'Save selection' },
{ value: 'refine', label: 'Refine: choose individual tables' },
{ value: 'back', label: 'Back' },
],
});
if (action === 'back') {
continue;
}
const discovered = await args.listTablesForSchemas(selectedSchemas);
if (action === 'save' && args.existing.enabledTables.length === 0) {
return {
kind: 'selected',
activeSchemas: args.supportsSchemaScope ? selectedSchemas : [],
enabledTables: discovered.map(qualifiedTableId),
};
}
const refined = await runStageTwoTreePicker({
args,
discovered,
selectedSchemas,
io,
render,
});
if (refined.kind === 'back') {
continue;
}
return refined;
}
}

View file

@ -42,7 +42,7 @@ function makeIo() {
type ScopePick =
| 'back'
| 'enable-all'
| { schemas: string[]; tables: string[] };
| { schemas: string[]; tables: string[] | 'back' };
interface PickerStubs {
pickDatabaseScope: KtxSetupDatabasesDeps['pickDatabaseScope'];
@ -58,15 +58,21 @@ function makePickerStubs(options: { scopes?: ScopePick[] } = {}): PickerStubs {
scopeCalls.push(args);
const next = queue.shift();
if (next === undefined || next === 'enable-all') {
const enabledTables = args.discovered.map((t) => `${t.schema}.${t.name}`);
const schemas = args.initialSchemas && args.initialSchemas.length > 0 ? [...args.initialSchemas] : [...args.schemas];
const discovered = await args.listTablesForSchemas(schemas);
const enabledTables = discovered.map((t) => `${t.schema}.${t.name}`);
const activeSchemas = args.supportsSchemaScope
? Array.from(new Set(args.discovered.map((t) => t.schema)))
? Array.from(new Set(discovered.map((t) => t.schema)))
: [];
return { kind: 'selected', activeSchemas, enabledTables };
}
if (next === 'back') {
return { kind: 'back' };
}
await args.listTablesForSchemas(next.schemas);
if (next.tables === 'back') {
return { kind: 'back' };
}
return {
kind: 'selected',
activeSchemas: args.supportsSchemaScope ? next.schemas : [],
@ -88,7 +94,19 @@ function makePromptAdapter(options: {
const passwordValues = [...(options.passwordValues ?? [])];
return {
multiselect: vi.fn(async () => multiselectValues.shift() ?? ['postgres']),
autocompleteMultiselect: vi.fn(async (options) => {
if (multiselectValues.length > 0) {
return multiselectValues.shift() ?? [];
}
if (options.initialValues && options.initialValues.length > 0) {
return options.initialValues;
}
return options.options.length > 0 ? options.options.map((option) => option.value) : ['back'];
}),
select: vi.fn(async ({ message }) => {
if (message.startsWith('Save ') && message.includes(' or refine tables?')) {
return 'save';
}
if (message.includes('How much database context should KTX build?')) {
const nextValue = selectValues[0];
return nextValue === 'fast' || nextValue === 'deep' || nextValue === 'back'
@ -915,7 +933,7 @@ describe('setup databases step', () => {
placeholder: 'env:DATABASE_URL',
initialValue: 'env:DATABASE_URL',
});
expect(listTables).toHaveBeenCalledWith(tempDir, 'warehouse', ['analytics', 'public']);
expect(listTables).toHaveBeenCalledWith(tempDir, 'warehouse', ['analytics']);
expect(testConnection).toHaveBeenCalledWith(tempDir, 'warehouse', expect.anything());
expect(scanConnection).toHaveBeenCalledWith(tempDir, 'warehouse', expect.anything());
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
@ -1105,7 +1123,7 @@ describe('setup databases step', () => {
{ schema: 'public', name: 'customers', kind: 'table' as const },
{ schema: 'public', name: 'orders', kind: 'table' as const },
]);
const pickers = makePickerStubs({ scopes: ['back'] });
const pickers = makePickerStubs({ scopes: [{ schemas: ['public'], tables: 'back' }] });
const result = await runKtxSetupDatabasesStep(
{ projectDir: tempDir, inputMode: 'auto', skipDatabases: false, databaseSchemas: [] },
@ -1469,7 +1487,7 @@ describe('setup databases step', () => {
schemaSuggestion: { suggested: Set<string> };
};
expect(args.schemaNoun).toBe('database');
expect(args.discovered.map((table) => table.schema)).toEqual(['analytics', 'mart']);
expect(args.schemas).toEqual(['analytics', 'mart']);
expect(scopedArgs.schemaSuggestion.suggested).toEqual(new Set(['analytics', 'mart']));
return { kind: 'selected' as const, activeSchemas: ['mart'], enabledTables: ['mart.orders'] };
});
@ -1591,7 +1609,8 @@ describe('setup databases step', () => {
connectionId: 'postgres-warehouse',
schemaNoun: 'schema',
schemaNounPlural: 'schemas',
defaultSchemas: ['orbit_analytics', 'orbit_raw'],
schemas: ['orbit_analytics', 'orbit_raw', 'public'],
schemaSuggestion: { excluded: new Set(), suggested: new Set() },
});
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
expect(config.connections['postgres-warehouse']).toMatchObject({
@ -1600,6 +1619,41 @@ describe('setup databases step', () => {
expect(io.stdout()).toContain('✓ orbit_analytics, orbit_raw');
});
it('passes schemas and a lazy table callback to the scope picker instead of eager table discovery', async () => {
const listSchemas = vi.fn(async () => ['analytics', 'raw']);
const listTables = vi.fn(async (_projectDir: string, _connectionId: string, schemas?: string[]) =>
(schemas ?? []).map((schema) => ({ schema, name: 'orders', kind: 'table' as const })),
);
const pickDatabaseScope = vi.fn(async (args: PickDatabaseScopeArgs) => {
const lazyArgs = args as PickDatabaseScopeArgs & {
schemas: string[];
listTablesForSchemas: (schemas: string[]) => Promise<Array<{ schema: string; name: string; kind: 'table' }>>;
};
expect(lazyArgs.schemas).toEqual(['analytics', 'raw']);
expect(args).not.toHaveProperty('discovered');
expect(listTables).not.toHaveBeenCalled();
const tables = await lazyArgs.listTablesForSchemas(['analytics']);
expect(tables).toEqual([{ schema: 'analytics', name: 'orders', kind: 'table' }]);
return { kind: 'selected' as const, activeSchemas: ['analytics'], enabledTables: ['analytics.orders'] };
});
await runKtxSetupDatabasesStep(
{ projectDir: tempDir, inputMode: 'auto', databaseDrivers: ['postgres'], skipDatabases: false, databaseSchemas: [] },
makeIo().io,
{
prompts: makePromptAdapter({ selectValues: ['url'], textValues: ['', 'env:DATABASE_URL'] }),
testConnection: vi.fn(async () => 0),
scanConnection: vi.fn(async () => 0),
listSchemas,
listTables,
pickDatabaseScope,
},
);
expect(listTables).toHaveBeenCalledTimes(1);
expect(listTables).toHaveBeenCalledWith(tempDir, 'postgres-warehouse', ['analytics']);
});
it('auto-selects all discovered Postgres schemas in non-interactive setup', async () => {
const io = makeIo();
const prompts = makePromptAdapter({});

View file

@ -69,6 +69,14 @@ export interface KtxSetupDatabasesPromptAdapter {
initialValues?: string[];
}): Promise<string[]>;
select(options: { message: string; options: KtxSetupPromptOption[] }): Promise<string>;
autocompleteMultiselect(options: {
message: string;
options: KtxSetupPromptOption[];
placeholder?: string;
required?: boolean;
maxItems?: number;
initialValues?: string[];
}): Promise<string[]>;
text(options: { message: string; placeholder?: string; initialValue?: string }): Promise<string | undefined>;
password(options: { message: string }): Promise<string | undefined>;
cancel(message: string): void;
@ -156,19 +164,6 @@ function defaultSuggest(values: string[]): ScopeSuggestion {
return { excluded, suggested };
}
function legacyDefaultSchemasForPicker(
schemas: string[],
suggestion: ScopeSuggestion,
): string[] {
const suggested = schemas.filter((schema) => suggestion.suggested.has(schema));
if (suggested.length > 0) {
return suggested;
}
const visible = schemas.filter((schema) => !suggestion.excluded.has(schema));
const nonPublic = visible.filter((schema) => schema !== 'public' && schema !== 'PUBLIC');
return nonPublic.length > 0 ? nonPublic : visible;
}
const SCOPE_DISCOVERY_SPECS: Partial<Record<KtxSetupDatabaseDriver, ScopeDiscoverySpec>> = {
postgres: {
noun: 'schema',
@ -1413,6 +1408,7 @@ async function maybeConfigureDatabaseScope(input: {
args: KtxSetupDatabasesArgs;
deps: KtxSetupDatabasesDeps;
io: KtxCliIo;
prompts: KtxSetupDatabasesPromptAdapter;
forcePrompt?: boolean;
}): Promise<ConnectionSetupStatus> {
const project = await loadKtxProject({ projectDir: input.projectDir });
@ -1473,32 +1469,53 @@ async function maybeConfigureDatabaseScope(input: {
});
}
writeSetupSection(input.io, 'Discovering tables', [
`Connecting to ${input.connectionId}`,
]);
writeSetupSection(input.io, 'Discovering tables', [`Connecting to ${input.connectionId}`]);
const schemasFilter = await (async (): Promise<string[]> => {
if (cliSchemas.length > 0) return cliSchemas;
if (!spec) return [];
try {
return unique(
await (input.deps.listSchemas ?? defaultListSchemas)(input.projectDir, input.connectionId),
);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
input.io.stderr.write(
`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`,
);
return [];
}
})();
const schemas = unique(
cliSchemas.length > 0
? cliSchemas
: await (async (): Promise<string[]> => {
if (!spec) return [];
try {
return await (input.deps.listSchemas ?? defaultListSchemas)(input.projectDir, input.connectionId);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
input.io.stderr.write(
`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`,
);
return [];
}
})(),
);
if (spec && schemas.length === 0) {
return 'ready';
}
const schemaSuggestion =
cliSchemas.length > 0
? { excluded: new Set<string>(), suggested: new Set(cliSchemas) }
: spec?.suggest(schemas) ?? { excluded: new Set<string>(), suggested: new Set<string>() };
const existingEnabled =
hasExistingTables && input.forcePrompt === true
? (existingTables ?? []).filter((table): table is string => typeof table === 'string')
: [];
let discovered: KtxTableListEntry[];
let pickResult: DatabaseScopePickResult;
try {
discovered = await (input.deps.listTables ?? defaultListTables)(
input.projectDir,
input.connectionId,
schemasFilter.length > 0 ? schemasFilter : undefined,
pickResult = await (input.deps.pickDatabaseScope ?? defaultPickDatabaseScope)(
{
connectionId: input.connectionId,
schemaNoun: spec?.noun ?? 'schema',
schemaNounPlural: spec?.nounPlural ?? 'schemas',
schemas,
schemaSuggestion,
existing: { enabledTables: existingEnabled },
supportsSchemaScope: spec !== undefined,
initialSchemas: cliSchemas.length > 0 ? cliSchemas : undefined,
prompts: input.prompts,
listTablesForSchemas: (selectedSchemas) =>
(input.deps.listTables ?? defaultListTables)(input.projectDir, input.connectionId, selectedSchemas),
},
input.io,
);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
@ -1509,60 +1526,11 @@ async function maybeConfigureDatabaseScope(input: {
);
return input.forcePrompt === true ? 'failed' : 'ready';
}
if (discovered.length === 0) {
if (input.forcePrompt === true) {
input.io.stderr.write(`No tables discovered for ${input.connectionId}; edit was not saved.\n`);
}
return input.forcePrompt === true ? 'failed' : 'ready';
}
const allQualified = discovered.map((t) => `${t.schema}.${t.name}`);
const schemasInDiscovery = unique(discovered.map((t) => t.schema));
const defaultSchemas = (() => {
if (cliSchemas.length > 0) return cliSchemas;
if (!spec) return schemasInDiscovery;
const suggestion = spec.suggest(schemasInDiscovery);
return legacyDefaultSchemasForPicker(schemasInDiscovery, suggestion);
})();
const schemaSuggestion = cliSchemas.length > 0
? { excluded: new Set<string>(), suggested: new Set(cliSchemas) }
: spec?.suggest(schemasInDiscovery);
const existingEnabled =
hasExistingTables && input.forcePrompt === true
? (existingTables ?? []).filter(
(table): table is string => typeof table === 'string' && allQualified.includes(table),
)
: [];
let activeSchemas: string[];
let enabledTables: string[];
if (discovered.length === 1) {
enabledTables = allQualified;
activeSchemas = spec ? schemasInDiscovery : [];
} else {
const pickResult = await (input.deps.pickDatabaseScope ?? defaultPickDatabaseScope)(
{
connectionId: input.connectionId,
schemaNoun: spec?.noun ?? 'schema',
schemaNounPlural: spec?.nounPlural ?? 'schemas',
discovered,
existing: { enabledTables: existingEnabled },
defaultSchemas,
...(schemaSuggestion ? { schemaSuggestion } : {}),
supportsSchemaScope: spec !== undefined,
},
input.io,
);
if (pickResult.kind === 'back') {
return 'back';
}
enabledTables = pickResult.enabledTables;
activeSchemas = pickResult.activeSchemas;
if (pickResult.kind === 'back') {
return 'back';
}
const enabledTables = pickResult.enabledTables;
const activeSchemas = pickResult.activeSchemas;
if (spec) {
await writeScopeConfig({
@ -1588,7 +1556,7 @@ async function maybeConfigureDatabaseScope(input: {
]);
}
writeSetupSection(input.io, `Tables enabled for ${input.connectionId}`, [
`${enabledTables.length}/${discovered.length} tables enabled`,
`${enabledTables.length} tables enabled`,
]);
return 'ready';
}