fix(status): report query history readiness

This commit is contained in:
Andrey Avtomonov 2026-05-13 19:06:32 +02:00
parent bc23b1a447
commit f78c49509f
3 changed files with 83 additions and 45 deletions

View file

@ -266,7 +266,7 @@ describe('runKtxDoctor', () => {
expect(testIo.stdout()).toContain('PASS Connections: 1 configured');
});
it('includes Postgres historic-SQL readiness in project doctor output', async () => {
it('includes Postgres query-history readiness in project doctor output', async () => {
await writeFile(
join(tempDir, 'ktx.yaml'),
[
@ -276,9 +276,9 @@ describe('runKtxDoctor', () => {
' driver: postgres',
' url: env:WAREHOUSE_DATABASE_URL',
' readonly: true',
' historicSql:',
' enabled: true',
' dialect: postgres',
' context:',
' queryHistory:',
' enabled: true',
'ingest:',
' adapters:',
' - live-database',
@ -290,8 +290,8 @@ describe('runKtxDoctor', () => {
const testIo = makeIo();
const runHistoricSqlDoctorChecks = vi.fn(async () => [
{
id: 'historic-sql-postgres-warehouse',
label: 'Postgres Historic SQL (warehouse)',
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'pass' as const,
detail:
'pg_stat_statements ready (PostgreSQL 16.4); info: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
@ -312,7 +312,7 @@ describe('runKtxDoctor', () => {
).resolves.toBe(0);
expect(runHistoricSqlDoctorChecks).toHaveBeenCalledTimes(1);
expect(testIo.stdout()).toContain('PASS Postgres Historic SQL (warehouse): pg_stat_statements ready');
expect(testIo.stdout()).toContain('PASS Postgres query history (warehouse): pg_stat_statements ready');
expect(testIo.stdout()).toContain('info: pg_stat_statements.max is 1000');
expect(testIo.stdout()).not.toContain('Fix: Update the Postgres parameter group or config');
});

View file

@ -22,7 +22,7 @@ function projectWithConnections(connections: Record<string, KtxProjectConnection
}
describe('runPostgresHistoricSqlDoctorChecks', () => {
it('passes when no Postgres historic-SQL connections are enabled', async () => {
it('passes when no Postgres query-history connections are enabled', async () => {
const checks = await runPostgresHistoricSqlDoctorChecks(
projectWithConnections({
warehouse: { driver: 'sqlite', path: './warehouse.db', readonly: true },
@ -34,10 +34,10 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
expect(checks).toEqual([
{
id: 'historic-sql-postgres',
label: 'Postgres Historic SQL',
id: 'query-history-postgres',
label: 'Postgres query history',
status: 'pass',
detail: 'No enabled Postgres historic-SQL connections',
detail: 'No enabled Postgres query-history connections',
},
]);
});
@ -54,7 +54,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
context: { queryHistory: { enabled: true } },
},
}),
{ postgresHistoricSqlProbe: probe },
@ -67,14 +67,14 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
context: { queryHistory: { enabled: true } },
},
env: process.env,
});
expect(checks).toEqual([
{
id: 'historic-sql-postgres-warehouse',
label: 'Postgres Historic SQL (warehouse)',
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'pass',
detail: 'pg_stat_statements ready (PostgreSQL 16.4)',
},
@ -88,7 +88,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
context: { queryHistory: { enabled: true } },
},
}),
{
@ -104,8 +104,8 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
expect(checks).toEqual([
{
id: 'historic-sql-postgres-warehouse',
label: 'Postgres Historic SQL (warehouse)',
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'pass',
detail:
'pg_stat_statements ready (PostgreSQL 16.4); info: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
@ -120,7 +120,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
context: { queryHistory: { enabled: true } },
},
}),
{
@ -138,8 +138,8 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
expect(checks).toEqual([
{
id: 'historic-sql-postgres-warehouse',
label: 'Postgres Historic SQL (warehouse)',
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'warn',
detail:
'pg_stat_statements ready (PostgreSQL 16.4) with warnings: pg_stat_statements.track is none; set it to top or all in the Postgres parameter group or config; info: pg_stat_statements.max is 1000; set it to at least 5000 to reduce query-template eviction churn',
@ -148,14 +148,42 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
]);
});
it('fails when a connection has postgres historic SQL but is not a Postgres driver', async () => {
it('still checks legacy historicSql blocks before setup migration', async () => {
const probe = vi.fn<PostgresHistoricSqlDoctorProbe>(async () => ({
pgServerVersion: 'PostgreSQL 16.4',
warnings: [],
}));
const checks = await runPostgresHistoricSqlDoctorChecks(
projectWithConnections({
warehouse: {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
},
}),
{ postgresHistoricSqlProbe: probe },
);
expect(checks).toEqual([
{
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'pass',
detail: 'pg_stat_statements ready (PostgreSQL 16.4)',
},
]);
});
it('fails when a connection has postgres query history but is not a Postgres driver', async () => {
const checks = await runPostgresHistoricSqlDoctorChecks(
projectWithConnections({
warehouse: {
driver: 'mysql',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
context: { queryHistory: { enabled: true } },
},
}),
{
@ -165,11 +193,11 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
expect(checks).toEqual([
{
id: 'historic-sql-postgres-warehouse',
label: 'Postgres Historic SQL (warehouse)',
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'fail',
detail: 'connections.warehouse.historicSql.dialect is postgres but driver is mysql',
fix: 'Set connections.warehouse.driver to postgres or disable historicSql for this connection',
detail: 'connections.warehouse.context.queryHistory is enabled but driver is mysql',
fix: 'Set connections.warehouse.driver to postgres or disable query history for this connection',
},
]);
});
@ -181,7 +209,7 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
driver: 'postgres',
url: 'env:WAREHOUSE_DATABASE_URL',
readonly: true,
historicSql: { enabled: true, dialect: 'postgres' },
context: { queryHistory: { enabled: true } },
},
}),
{
@ -197,8 +225,8 @@ describe('runPostgresHistoricSqlDoctorChecks', () => {
expect(checks).toEqual([
{
id: 'historic-sql-postgres-warehouse',
label: 'Postgres Historic SQL (warehouse)',
id: 'query-history-postgres-warehouse',
label: 'Postgres query history (warehouse)',
status: 'fail',
detail: 'pg_stat_statements extension is not installed in the connection database.',
fix: 'Run CREATE EXTENSION pg_stat_statements; against the connection database.',

View file

@ -32,16 +32,26 @@ function check(status: DoctorCheck['status'], id: string, label: string, detail:
return fix ? { id, label, status, detail, fix } : { id, label, status, detail };
}
function historicSqlRecord(connection: KtxProjectConnectionConfig): Record<string, unknown> | null {
const historicSql = connection.historicSql;
return historicSql && typeof historicSql === 'object' && !Array.isArray(historicSql)
? (historicSql as Record<string, unknown>)
: null;
function recordValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, unknown>) : null;
}
function isEnabledPostgresHistoricSql(connection: KtxProjectConnectionConfig): boolean {
const historicSql = historicSqlRecord(connection);
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
function queryHistoryRecord(connection: KtxProjectConnectionConfig): Record<string, unknown> | null {
const context = recordValue(connection.context);
return recordValue(context?.queryHistory);
}
function legacyHistoricSqlRecord(connection: KtxProjectConnectionConfig): Record<string, unknown> | null {
return recordValue(connection.historicSql);
}
function isEnabledPostgresQueryHistory(connection: KtxProjectConnectionConfig): boolean {
const queryHistory = queryHistoryRecord(connection);
if (queryHistory) {
return queryHistory.enabled === true;
}
const legacy = legacyHistoricSqlRecord(connection);
return legacy?.enabled === true && legacy.dialect === 'postgres';
}
function isPostgresDriver(connection: KtxProjectConnectionConfig): boolean {
@ -50,7 +60,7 @@ function isPostgresDriver(connection: KtxProjectConnectionConfig): boolean {
}
function checkId(connectionId: string): string {
return `historic-sql-postgres-${connectionId.replace(/[^a-z0-9_-]+/gi, '-')}`;
return `query-history-postgres-${connectionId.replace(/[^a-z0-9_-]+/gi, '-')}`;
}
function capabilityFailureFix(error: unknown, connectionId: string, projectDir: string): string {
@ -61,7 +71,7 @@ function capabilityFailureFix(error: unknown, connectionId: string, projectDir:
return String(error.remediation);
}
if (error instanceof Error && error.name === 'HistoricSqlVersionUnsupportedError') {
return 'Use PostgreSQL 14 or newer, or disable historicSql for this connection';
return 'Use PostgreSQL 14 or newer, or disable query history for this connection';
}
return `Fix connections.${connectionId} Postgres settings, then rerun \`ktx status --project-dir ${projectDir}\``;
}
@ -107,12 +117,12 @@ export async function runPostgresHistoricSqlDoctorChecks(
deps: HistoricSqlDoctorDeps = {},
): Promise<DoctorCheck[]> {
const targets = Object.entries(project.config.connections)
.filter(([, connection]) => isEnabledPostgresHistoricSql(connection))
.filter(([, connection]) => isEnabledPostgresQueryHistory(connection))
.sort(([left], [right]) => left.localeCompare(right));
if (targets.length === 0) {
return [
check('pass', 'historic-sql-postgres', 'Postgres Historic SQL', 'No enabled Postgres historic-SQL connections'),
check('pass', 'query-history-postgres', 'Postgres query history', 'No enabled Postgres query-history connections'),
];
}
@ -120,15 +130,15 @@ export async function runPostgresHistoricSqlDoctorChecks(
const env = deps.env ?? process.env;
const checks: DoctorCheck[] = [];
for (const [connectionId, connection] of targets) {
const label = `Postgres Historic SQL (${connectionId})`;
const label = `Postgres query history (${connectionId})`;
if (!isPostgresDriver(connection)) {
checks.push(
check(
'fail',
checkId(connectionId),
label,
`connections.${connectionId}.historicSql.dialect is postgres but driver is ${String(connection.driver)}`,
`Set connections.${connectionId}.driver to postgres or disable historicSql for this connection`,
`connections.${connectionId}.context.queryHistory is enabled but driver is ${String(connection.driver)}`,
`Set connections.${connectionId}.driver to postgres or disable query history for this connection`,
),
);
continue;