Fix database setup edit preservation

This commit is contained in:
Andrey Avtomonov 2026-05-22 00:17:06 +02:00
parent 359d46e230
commit cd1bd27e24
5 changed files with 96 additions and 5 deletions

View file

@ -283,10 +283,12 @@ export async function pickDatabaseScope(
continue;
}
const selectedNoun =
selectedSchemas.length === 1 ? args.schemaNoun : args.schemaNounPlural;
const action = await args.prompts.select({
message: `Save ${selectedSchemas.length} ${selectedSchemas.length === 1 ? args.schemaNoun : args.schemaNounPlural} or refine tables?`,
message: `Enable all tables in ${selectedSchemas.length} ${selectedNoun}, or refine tables?`,
options: [
{ value: 'save', label: 'Save selection' },
{ value: 'save', label: `Enable all tables in selected ${selectedNoun}` },
{ value: 'refine', label: 'Refine: choose individual tables' },
{ value: 'back', label: 'Back' },
],

View file

@ -167,6 +167,40 @@ describe('CLI local ingest adapters', () => {
]);
});
it('resolves BigQuery credentials_json from a file: reference for query history ingest', async () => {
const credentialsPath = join(tempDir, 'credentials.json');
await writeFile(credentialsPath, JSON.stringify({ project_id: 'demo-project' }), 'utf-8');
await writeProject(
tempDir,
[
'connections:',
' bq:',
' driver: bigquery',
' dataset_id: analytics',
' location: us',
` credentials_json: 'file:${credentialsPath}'`,
' historicSql:',
' enabled: true',
' dialect: bigquery',
'ingest:',
' adapters:',
' - historic-sql',
'',
].join('\n'),
);
const project = await loadKtxProject({ projectDir: tempDir });
const adapters = createKtxCliLocalIngestAdapters(project, {
historicSqlConnectionId: 'bq',
sqlAnalysis: sqlAnalysisStub(),
});
expect(adapters.find((adapter) => adapter.source === 'historic-sql')?.skillNames).toEqual([
'historic_sql_table_digest',
'historic_sql_patterns',
]);
});
it('uses query-history wording for public BigQuery capability errors', async () => {
await writeProject(
tempDir,

View file

@ -30,6 +30,7 @@ import {
type ManagedPythonCoreDaemonOptions,
} from './managed-python-http.js';
import type { KtxOperationalLogger } from './io/logger.js';
import { resolveKtxConfigReference } from './context/core/config-reference.js';
function hasSnowflakeDriver(connection: unknown): boolean {
return (
@ -279,7 +280,10 @@ async function createEphemeralSnowflakeHistoricSqlClient(
function bigQueryProjectId(connection: KtxBigQueryConnectionConfig, env: NodeJS.ProcessEnv): string {
const raw = typeof connection.credentials_json === 'string' ? connection.credentials_json : '';
const resolved = raw.startsWith('env:') ? env[raw.slice('env:'.length)] ?? '' : raw;
const resolved = resolveKtxConfigReference(raw, env);
if (!resolved) {
throw new Error('Query history BigQuery connection requires credentials_json');
}
const parsed = JSON.parse(resolved) as { project_id?: unknown };
if (typeof parsed.project_id !== 'string' || parsed.project_id.trim().length === 0) {
throw new Error('Query history BigQuery connection requires credentials_json.project_id');

View file

@ -106,7 +106,7 @@ function makePromptAdapter(options: {
: ['back'];
}),
select: vi.fn(async ({ message }) => {
if (message.startsWith('Save ') && message.includes(' or refine tables?')) {
if (message.startsWith('Enable all tables in ') && message.includes(', or refine tables?')) {
return 'save';
}
if (message.includes('How much database context should KTX build?')) {
@ -260,6 +260,48 @@ describe('setup databases step', () => {
expect(prompts.select).toHaveBeenCalledTimes(1);
});
it('preserves context.depth when editing an existing database connection', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
[
'connections:',
' warehouse:',
' driver: sqlite',
' path: ./warehouse.sqlite',
' context:',
' depth: deep',
'',
].join('\n'),
'utf-8',
);
const prompts = makePromptAdapter({
selectValues: ['edit', 'warehouse', 'continue'],
textValues: ['./warehouse.sqlite'],
});
const testConnection = vi.fn(async () => 0);
const scanConnection = vi.fn(async () => 0);
const io = makeIo();
const result = await runKtxSetupDatabasesStep(
{
projectDir: tempDir,
inputMode: 'auto',
skipDatabases: false,
databaseSchemas: [],
disableQueryHistory: true,
},
io.io,
{ prompts, testConnection, scanConnection },
);
expect(result.status, io.stderr()).toBe('ready');
const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
expect(config.connections.warehouse).toMatchObject({
driver: 'sqlite',
path: './warehouse.sqlite',
context: { depth: 'deep' },
});
});
it('labels existing database connections with the database type', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),

View file

@ -663,6 +663,12 @@ function normalizeFileReference(value: string): string {
return `file:${normalized}`;
}
function displayFileReference(value: string | undefined): string | undefined {
if (value === undefined) return undefined;
if (value.startsWith('file:')) return value.slice('file:'.length);
return value;
}
function scriptedScopeConfigForDriver(
driver: KtxSetupDatabaseDriver,
databaseSchemas: string[],
@ -910,7 +916,7 @@ async function buildConnectionConfig(input: {
const credentialsPath = await promptText(
prompts,
'Path to service account JSON file',
stringConfigField(input.existingConnection, 'credentials_json'),
displayFileReference(stringConfigField(input.existingConnection, 'credentials_json')),
);
if (credentialsPath === undefined) return 'back';
const location = await promptText(
@ -1359,6 +1365,9 @@ function withExistingPrimaryEditPromptDefaults(input: {
if (!Object.hasOwn(input.next, 'enabled_tables') && Array.isArray(input.previous.enabled_tables)) {
merged.enabled_tables = input.previous.enabled_tables;
}
if (!Object.hasOwn(input.next, 'context') && input.previous.context !== undefined) {
merged.context = input.previous.context;
}
return merged;
}