feat(connectors): generalize readiness and constraint handling (#212)

* feat(connectors): add postgres maxConnections

* feat(connectors): add mysql maxConnections

* feat(connectors): add sqlserver maxConnections

* feat(connectors): rename snowflake pool config

* docs: document connector maxConnections

* feat(scan): add constraint discovery warning helper

* feat(scan): carry structural warnings through reports

* feat(postgres): soft-fail denied constraint discovery

* feat(mysql): soft-fail denied constraint discovery

* feat(sqlserver): soft-fail denied constraint discovery

* feat(bigquery): soft-fail denied primary key discovery

* feat(snowflake): report denied primary key discovery

* test(scan): verify constraint discovery warnings

* feat(historic-sql): use shared readiness probes

* docs: document query history readiness probes

* test(historic-sql): verify readiness probe registry

* test(ingest): account for live database warnings artifact

* Add skip option for agent setup
This commit is contained in:
Andrey Avtomonov 2026-05-24 19:30:06 +02:00 committed by GitHub
parent cfd1749ab9
commit 78b8a0c025
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 2763 additions and 554 deletions

View file

@ -4,11 +4,15 @@ import { runClaudeCodeAuthProbe } from './context/llm/claude-code-runtime.js';
import type { KtxConfigIssue, KtxProjectConfig, KtxProjectConnectionConfig, KtxProjectEmbeddingConfig, KtxProjectLlmConfig } from './context/project/config.js';
import type { KtxLocalProject } from './context/project/project.js';
import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
import type { PostgresPgssProbeResult } from './context/ingest/adapters/historic-sql/types.js';
import {
isQueryHistoryEnabled,
queryHistoryDialectForConnection,
} from './context/ingest/adapters/historic-sql/connection-dialect.js';
import {
historicSqlProbeCatalogName,
runHistoricSqlReadinessProbe,
type HistoricSqlReadinessProbe,
} from './context/ingest/historic-sql-probes.js';
import {
formatClaudeCodePromptCachingFix,
formatClaudeCodePromptCachingWarning,
@ -170,6 +174,13 @@ function resolveRef(value: unknown, env: NodeJS.ProcessEnv): { resolved: string;
return { resolved: trimmed, via: 'literal' };
}
function failureDetail(error: unknown): string {
if (error instanceof Error && error.message.trim().length > 0) {
return error.message.trim().split('\n')[0] ?? error.message.trim();
}
return String(error);
}
function envHint(value: unknown): string | undefined {
if (typeof value === 'string' && value.trim().startsWith('env:')) {
return value.trim().slice(4).trim();
@ -392,232 +403,6 @@ function buildConnectionStatus(
}
}
interface QueryHistoryProbeInput {
projectDir: string;
connectionId: string;
connection: KtxProjectConnectionConfig;
env: NodeJS.ProcessEnv;
}
interface GenericProbeResult {
warnings: string[];
info?: string[];
}
type PostgresQueryHistoryProbe = (input: QueryHistoryProbeInput) => Promise<PostgresPgssProbeResult>;
type SnowflakeQueryHistoryProbe = (input: QueryHistoryProbeInput) => Promise<GenericProbeResult>;
type BigQueryQueryHistoryProbe = (input: QueryHistoryProbeInput) => Promise<GenericProbeResult>;
function failureDetail(error: unknown): string {
if (error instanceof Error && error.message.trim().length > 0) {
return error.message.trim().split('\n')[0] ?? error.message.trim();
}
return String(error);
}
function postgresReadinessDetail(result: PostgresPgssProbeResult): string {
const warningText = result.warnings.length > 0 ? ` with warnings: ${result.warnings.join('; ')}` : '';
const info = result.info ?? [];
const infoText = info.length > 0 ? `; info: ${info.join('; ')}` : '';
return `pg_stat_statements ready (${result.pgServerVersion})${warningText}${infoText}`;
}
function genericReadinessDetail(label: string, result: GenericProbeResult): string {
const warningText = result.warnings.length > 0 ? ` with warnings: ${result.warnings.join('; ')}` : '';
const info = result.info ?? [];
const infoText = info.length > 0 ? `; info: ${info.join('; ')}` : '';
return `${label} ready${warningText}${infoText}`;
}
function probeFailureFix(error: unknown, dialect: string, connectionId: string, projectDir: string): string {
if (error instanceof Error && error.name === 'HistoricSqlExtensionMissingError' && 'remediation' in error) {
return String(error.remediation);
}
if (error instanceof Error && error.name === 'HistoricSqlGrantsMissingError' && 'remediation' in error) {
return String(error.remediation);
}
if (error instanceof Error && error.name === 'HistoricSqlVersionUnsupportedError') {
return 'Use PostgreSQL 14 or newer, or disable query history for this connection';
}
return `Fix connections.${connectionId} ${dialect} settings, then rerun \`ktx status --project-dir ${projectDir}\``;
}
async function defaultPostgresQueryHistoryProbe(
input: QueryHistoryProbeInput,
): Promise<PostgresPgssProbeResult> {
const [{ PostgresPgssReader }, { KtxPostgresHistoricSqlQueryClient }, { isKtxPostgresConnectionConfig }] =
await Promise.all([
import('./context/ingest/adapters/historic-sql/postgres-pgss-reader.js'),
import('./connectors/postgres/historic-sql-query-client.js'),
import('./connectors/postgres/connector.js'),
]);
const inputDriver = input.connection.driver ?? 'unknown';
if (!isKtxPostgresConnectionConfig(input.connection)) {
throw new Error(`Native PostgreSQL connector cannot run driver "${inputDriver}"`);
}
const client = new KtxPostgresHistoricSqlQueryClient({
connectionId: input.connectionId,
connection: input.connection,
env: input.env,
});
try {
return await new PostgresPgssReader().probe(client);
} finally {
await client.cleanup();
}
}
async function defaultSnowflakeQueryHistoryProbe(
input: QueryHistoryProbeInput,
): Promise<GenericProbeResult> {
const [{ SnowflakeHistoricSqlQueryHistoryReader }, { KtxSnowflakeHistoricSqlQueryClient }, { isKtxSnowflakeConnectionConfig }] =
await Promise.all([
import('./context/ingest/adapters/historic-sql/snowflake-query-history-reader.js'),
import('./connectors/snowflake/historic-sql-query-client.js'),
import('./connectors/snowflake/connector.js'),
]);
const inputDriver = input.connection.driver ?? 'unknown';
if (!isKtxSnowflakeConnectionConfig(input.connection)) {
throw new Error(`Native Snowflake connector cannot run driver "${inputDriver}"`);
}
const client = new KtxSnowflakeHistoricSqlQueryClient({
connectionId: input.connectionId,
connection: input.connection,
projectDir: input.projectDir,
env: input.env,
});
try {
return await new SnowflakeHistoricSqlQueryHistoryReader().probe(client);
} finally {
await client.cleanup();
}
}
async function defaultBigQueryQueryHistoryProbe(
input: QueryHistoryProbeInput,
): Promise<GenericProbeResult> {
const [
{ BigQueryHistoricSqlQueryHistoryReader },
{ KtxBigQueryScanConnector, isKtxBigQueryConnectionConfig },
{ resolveKtxConfigReference },
] = await Promise.all([
import('./context/ingest/adapters/historic-sql/bigquery-query-history-reader.js'),
import('./connectors/bigquery/connector.js'),
import('./context/core/config-reference.js'),
]);
const inputDriver = input.connection.driver ?? 'unknown';
if (!isKtxBigQueryConnectionConfig(input.connection)) {
throw new Error(`Native BigQuery connector cannot run driver "${inputDriver}"`);
}
const rawCredentials = typeof input.connection.credentials_json === 'string' ? input.connection.credentials_json : '';
const resolvedCredentials = resolveKtxConfigReference(rawCredentials, input.env);
if (!resolvedCredentials) {
throw new Error(`Query history BigQuery connection ${input.connectionId} requires credentials_json`);
}
const parsed = JSON.parse(resolvedCredentials) as { project_id?: unknown };
if (typeof parsed.project_id !== 'string' || parsed.project_id.trim().length === 0) {
throw new Error(`Query history BigQuery connection ${input.connectionId} requires credentials_json.project_id`);
}
const region =
typeof input.connection.location === 'string' && input.connection.location.trim().length > 0
? input.connection.location.trim()
: 'us';
const connector = new KtxBigQueryScanConnector({
connectionId: input.connectionId,
connection: input.connection,
});
try {
return await new BigQueryHistoricSqlQueryHistoryReader({
projectId: parsed.project_id,
region,
}).probe({
async executeQuery(sql: string) {
const result = await connector.executeReadOnly({ connectionId: input.connectionId, sql }, {} as never);
return {
headers: result.headers,
rows: result.rows,
totalRows: result.totalRows,
};
},
});
} finally {
await connector.cleanup();
}
}
interface DispatchedProbe {
label: string;
spinnerLabel: string;
fastSkipDetail: string;
run: () => Promise<{ status: ProjectStatusLevel; detail: string; fix?: string }>;
}
function postgresProbeDispatch(
input: QueryHistoryProbeInput,
probe: PostgresQueryHistoryProbe,
): DispatchedProbe {
return {
label: 'postgres',
spinnerLabel: `Probing pg_stat_statements on ${input.connectionId}`,
fastSkipDetail: 'pg_stat_statements probe skipped (--fast)',
run: async () => {
const result = await probe(input);
return {
status: result.warnings.length > 0 ? 'warn' : 'ok',
detail: postgresReadinessDetail(result),
...(result.warnings.length > 0
? {
fix: `Update the Postgres parameter group or config, then rerun \`ktx status --project-dir ${input.projectDir}\``,
}
: {}),
};
},
};
}
function snowflakeProbeDispatch(
input: QueryHistoryProbeInput,
probe: SnowflakeQueryHistoryProbe,
): DispatchedProbe {
return {
label: 'snowflake',
spinnerLabel: `Probing SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY on ${input.connectionId}`,
fastSkipDetail: 'SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY probe skipped (--fast)',
run: async () => {
const result = await probe(input);
return {
status: result.warnings.length > 0 ? 'warn' : 'ok',
detail: genericReadinessDetail('SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY', result),
};
},
};
}
function bigqueryProbeDispatch(
input: QueryHistoryProbeInput,
probe: BigQueryQueryHistoryProbe,
): DispatchedProbe {
return {
label: 'bigquery',
spinnerLabel: `Probing INFORMATION_SCHEMA.JOBS_BY_PROJECT on ${input.connectionId}`,
fastSkipDetail: 'INFORMATION_SCHEMA.JOBS_BY_PROJECT probe skipped (--fast)',
run: async () => {
const result = await probe(input);
return {
status: result.warnings.length > 0 ? 'warn' : 'ok',
detail: genericReadinessDetail('INFORMATION_SCHEMA.JOBS_BY_PROJECT', result),
};
},
};
}
async function buildQueryHistoryStatus(
project: KtxLocalProject,
options: BuildProjectStatusOptions,
@ -626,9 +411,7 @@ async function buildQueryHistoryStatus(
.filter(([, connection]) => isQueryHistoryEnabled(connection))
.sort(([left], [right]) => left.localeCompare(right));
const postgresProbe = options.postgresQueryHistoryProbe ?? defaultPostgresQueryHistoryProbe;
const snowflakeProbe = options.snowflakeQueryHistoryProbe ?? defaultSnowflakeQueryHistoryProbe;
const bigqueryProbe = options.bigqueryQueryHistoryProbe ?? defaultBigQueryQueryHistoryProbe;
const probe = options.queryHistoryReadinessProbe ?? runHistoricSqlReadinessProbe;
const env = options.env ?? process.env;
const statuses: QueryHistoryStatus[] = [];
@ -648,18 +431,7 @@ async function buildQueryHistoryStatus(
continue;
}
const probeInput: QueryHistoryProbeInput = {
projectDir: project.projectDir,
connectionId,
connection,
env,
};
const dispatched =
dialect === 'postgres'
? postgresProbeDispatch(probeInput, postgresProbe)
: dialect === 'snowflake'
? snowflakeProbeDispatch(probeInput, snowflakeProbe)
: bigqueryProbeDispatch(probeInput, bigqueryProbe);
const catalogName = historicSqlProbeCatalogName(dialect);
if (options.fast === true) {
statuses.push({
@ -667,29 +439,61 @@ async function buildQueryHistoryStatus(
driver,
dialect,
status: 'skipped',
detail: dispatched.fastSkipDetail,
detail: `${catalogName} probe skipped (--fast)`,
});
continue;
}
try {
const outcome = await withSpinner(options.useSpinner === true, dispatched.spinnerLabel, dispatched.run);
const outcome = await withSpinner(
options.useSpinner === true,
`Probing ${catalogName} on ${connectionId}`,
() =>
probe({
projectDir: project.projectDir,
connectionId,
connection,
env,
}),
);
if (!outcome) {
statuses.push({
connection: connectionId,
driver,
dialect,
...outcome,
});
} catch (error) {
statuses.push({
connection: connectionId,
driver,
dialect,
dialect: driver,
status: 'fail',
detail: failureDetail(error),
fix: probeFailureFix(error, dispatched.label, connectionId, project.projectDir),
detail: `query history is not supported for driver "${driver}"`,
fix: `Disable connections.${connectionId}.context.queryHistory, or use a postgres, snowflake, or bigquery connection`,
});
continue;
}
if (outcome.ok) {
const { detail, warnings } = outcome.runner.formatSuccessDetail(outcome.result);
statuses.push({
connection: connectionId,
driver,
dialect,
status: warnings.length > 0 ? 'warn' : 'ok',
detail,
...(dialect === 'postgres' && warnings.length > 0
? {
fix: `Update the Postgres parameter group or config, then rerun \`ktx status --project-dir ${project.projectDir}\``,
}
: {}),
});
continue;
}
const advice = outcome.runner.fixAdvice(outcome.error);
statuses.push({
connection: connectionId,
driver,
dialect,
status: 'fail',
detail: advice.failHeadline,
fix: advice.remediation,
});
}
return statuses;
@ -828,9 +632,7 @@ function buildVerdict(
export interface BuildProjectStatusOptions {
env?: NodeJS.ProcessEnv;
postgresQueryHistoryProbe?: PostgresQueryHistoryProbe;
snowflakeQueryHistoryProbe?: SnowflakeQueryHistoryProbe;
bigqueryQueryHistoryProbe?: BigQueryQueryHistoryProbe;
queryHistoryReadinessProbe?: HistoricSqlReadinessProbe;
claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
configIssues?: KtxConfigIssue[];
fast?: boolean;