feat(setup): drop redundant Snowflake schema prompt; fall back to free-text on listSchemas failure

Snowflake setup previously asked for a single schema as free text, then
ran a multiselect against the discovered schemas — two schema questions
back-to-back, with the first being only a session bootstrap. The SDK's
`schema` is optional, so the bootstrap step is unnecessary.

- Remove the free-text Snowflake schema prompt; only pass `schema` to
  snowflake-sdk when one is configured.
- When `listSchemas()` fails (e.g. role lacks SHOW SCHEMAS), prompt the
  user for a comma-separated list, persist it as `schema_names`, and use
  it as both the table-list filter and the multiselect default. Applies
  to every driver with a scope-discovery spec, not just Snowflake.
- Update docs to lead with `schema_names`; keep `schema_name` as a
  documented single-schema shorthand.
This commit is contained in:
Andrey Avtomonov 2026-05-22 11:32:10 +02:00
parent fd2ba62d92
commit 70f47e8b54
4 changed files with 129 additions and 46 deletions

View file

@ -867,12 +867,6 @@ async function buildConnectionConfig(input: {
stringConfigField(input.existingConnection, 'database'),
);
if (database === undefined) return 'back';
const schemaName = await promptText(
prompts,
'Snowflake schema\nPress Enter for PUBLIC, or enter a schema name.',
stringConfigField(input.existingConnection, 'schema_name') ?? 'PUBLIC',
);
if (schemaName === undefined) return 'back';
const username = await promptText(
prompts,
'Snowflake username',
@ -894,14 +888,13 @@ async function buildConnectionConfig(input: {
);
if (role === undefined) return 'back';
const resolvedPasswordRef = passwordRef ?? stringConfigField(input.existingConnection, 'password');
if (!account || !warehouse || !database || !schemaName || !username || !resolvedPasswordRef) return null;
if (!account || !warehouse || !database || !username || !resolvedPasswordRef) return null;
return {
driver: 'snowflake',
authMethod: 'password',
account,
warehouse,
database,
schema_name: schemaName,
username,
password: resolvedPasswordRef,
...(role ? { role } : {}),
@ -1312,6 +1305,21 @@ async function writeScopeConfig(input: {
});
}
async function promptCommaSeparatedScope(input: {
prompts: KtxSetupDatabasesPromptAdapter;
connectionId: string;
spec: ScopeDiscoverySpec;
}): Promise<string[] | undefined> {
const example =
input.spec.nounPlural === 'datasets' ? 'sales, marketing' : 'SALES, MARKETING';
const value = await promptText(
input.prompts,
`Enter ${input.spec.nounPlural} for ${input.connectionId} as a comma-separated list (e.g. ${example}).`,
);
if (value === undefined) return undefined;
return unique(value.split(',').map((part) => part.trim()));
}
async function maybeConfigureDatabaseScope(input: {
projectDir: string;
connectionId: string;
@ -1382,11 +1390,15 @@ async function maybeConfigureDatabaseScope(input: {
`Connecting to ${input.connectionId}`,
]);
const schemasFilter = await (async (): Promise<string[]> => {
if (cliSchemas.length > 0) return cliSchemas;
if (!spec) return [];
let effectiveCliSchemas = cliSchemas;
let schemasFilter: string[];
if (effectiveCliSchemas.length > 0) {
schemasFilter = effectiveCliSchemas;
} else if (!spec) {
schemasFilter = [];
} else {
try {
return unique(
schemasFilter = unique(
await (input.deps.listSchemas ?? defaultListSchemas)(input.projectDir, input.connectionId),
);
} catch (error) {
@ -1394,9 +1406,21 @@ async function maybeConfigureDatabaseScope(input: {
input.io.stderr.write(
`Could not discover ${spec.promptLabel.toLowerCase()} for ${input.connectionId}; ${detail}\n`,
);
return [];
const prompts = input.deps.prompts ?? createPromptAdapter();
const typed = await promptCommaSeparatedScope({ prompts, connectionId: input.connectionId, spec });
if (typed === undefined) return 'back';
effectiveCliSchemas = typed;
schemasFilter = typed;
if (typed.length > 0) {
await writeScopeConfig({
projectDir: input.projectDir,
connectionId: input.connectionId,
values: typed,
spec,
});
}
}
})();
}
let discovered: KtxTableListEntry[];
try {
@ -1426,7 +1450,7 @@ async function maybeConfigureDatabaseScope(input: {
const schemasInDiscovery = unique(discovered.map((t) => t.schema));
const defaultSchemas = (() => {
if (cliSchemas.length > 0) return cliSchemas;
if (effectiveCliSchemas.length > 0) return effectiveCliSchemas;
if (!spec) return schemasInDiscovery;
return spec.defaultSelection(schemasInDiscovery);
})();