diff --git a/packages/cli/src/database-tree-picker.ts b/packages/cli/src/database-tree-picker.ts index 8630bc29..6b357de6 100644 --- a/packages/cli/src/database-tree-picker.ts +++ b/packages/cli/src/database-tree-picker.ts @@ -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' }, ], diff --git a/packages/cli/src/local-adapters.test.ts b/packages/cli/src/local-adapters.test.ts index ac8c3c41..efad86b6 100644 --- a/packages/cli/src/local-adapters.test.ts +++ b/packages/cli/src/local-adapters.test.ts @@ -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, diff --git a/packages/cli/src/local-adapters.ts b/packages/cli/src/local-adapters.ts index d5b7817e..88ee9880 100644 --- a/packages/cli/src/local-adapters.ts +++ b/packages/cli/src/local-adapters.ts @@ -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'); diff --git a/packages/cli/src/setup-databases.test.ts b/packages/cli/src/setup-databases.test.ts index 904f2c89..28c9e937 100644 --- a/packages/cli/src/setup-databases.test.ts +++ b/packages/cli/src/setup-databases.test.ts @@ -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'), diff --git a/packages/cli/src/setup-databases.ts b/packages/cli/src/setup-databases.ts index e26b6343..acdebeec 100644 --- a/packages/cli/src/setup-databases.ts +++ b/packages/cli/src/setup-databases.ts @@ -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; }