Skip to content

Commit 28af440

Browse files
committed
Merge branch 'fix/system-indexes-345' into 'main'
Index checks: never report system-catalog indexes (H001/H002/H004, rarely_used, index_definitions) Closes #345 See merge request postgres-ai/postgresai!407
2 parents 1062e69 + 6b40de9 commit 28af440

10 files changed

Lines changed: 1432 additions & 331 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22

33
## Unreleased
44

5+
- Fixed index health checks recommending that built-in catalog indexes be dropped. H001 (invalid indexes), H002 (unused indexes) and H004 (redundant indexes) — and the `unused_indexes`, `redundant_indexes`, `rarely_used_indexes` and `index_definitions` pgwatch metrics behind them — no longer report indexes in `pg_catalog`, `information_schema`, `pg_toast` or per-backend temp schemas. Such indexes cannot be dropped, so recommending their removal was always wrong; `pg_catalog.pg_class_tblspc_relfilenode_index` was the case that surfaced it. Expect a one-off step drop in unused/redundant index counts and total sizes on the first report after upgrading: the catalog rows that were being counted are simply gone. Bloat (F004/F005) and wraparound (F002) still include catalogs on purpose.
56
- Fixed express checkup on databases without the optional `postgres_ai` schema. F004/F005 now degrade with machine-readable status and warning summaries instead of reporting a misleading healthy empty result.

cli/lib/checkup.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ import { Client } from "pg";
5454
import * as fs from "fs";
5555
import * as path from "path";
5656
import * as pkg from "../package.json";
57-
import { getMetricSql, transformMetricRow, METRIC_NAMES } from "./metrics-loader";
57+
import { getMetricSql, transformMetricRow, isSystemSchema, METRIC_NAMES } from "./metrics-loader";
5858
import { buildCheckInfoMap } from "./checkup-dictionary";
5959
import { generateCheckSummary, CheckSummary } from "./checkup-summary";
6060

@@ -1058,7 +1058,9 @@ export async function getInvalidIndexes(client: Client, pgMajorVersion: number =
10581058
valid_duplicate_name: transformed.valid_index_name ? String(transformed.valid_index_name) : null,
10591059
valid_duplicate_definition: transformed.valid_index_definition ? String(transformed.valid_index_definition) : null,
10601060
};
1061-
});
1061+
// #345: the metric SQL is the primary filter; this second pass keeps
1062+
// catalog rows out of the report JSON if a future SQL edit re-admits them.
1063+
}).filter((index) => !isSystemSchema(index.schema_name));
10621064
}
10631065

10641066
/**
@@ -1087,7 +1089,8 @@ export async function getUnusedIndexes(client: Client, pgMajorVersion: number =
10871089
supports_fk: toBool(transformed.supports_fk),
10881090
index_size_pretty: formatBytes(indexSizeBytes),
10891091
};
1090-
});
1092+
// #345: never report catalog indexes (see isSystemSchema).
1093+
}).filter((index) => !isSystemSchema(index.schema_name));
10911094
}
10921095

10931096
/**
@@ -1242,7 +1245,8 @@ export async function getRedundantIndexes(client: Client, pgMajorVersion: number
12421245
}
12431246

12441247
return result;
1245-
});
1248+
// #345: never report catalog indexes (see isSystemSchema).
1249+
}).filter((index) => !isSystemSchema(index.schema_name));
12461250
}
12471251

12481252
/**

cli/lib/metrics-loader.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,33 @@ export function getMetricSql(metricName: string, pgMajorVersion: number = 16): s
2222
throw new Error(`Metric "${metricName}" not found. Available metrics: ${Object.keys(METRICS).join(", ")}`);
2323
}
2424

25-
// Find the best matching version: highest version <= pgMajorVersion
25+
// Find the best matching version: highest version <= pgMajorVersion.
26+
// Keep each original key next to its parsed number — parseInt("9.6") is 9,
27+
// and looking 9 back up would miss the "9.6" entry and yield undefined
28+
// instead of raising. Non-numeric keys are dropped rather than sorted as NaN.
2629
const availableVersions = Object.keys(metric.sqls)
27-
.map(v => parseInt(v, 10))
28-
.sort((a, b) => b - a); // Sort descending
30+
.map(key => ({ key, version: parseInt(key, 10) }))
31+
.filter(entry => Number.isFinite(entry.version))
32+
.sort((a, b) => b.version - a.version); // Sort descending
2933

30-
const matchingVersion = availableVersions.find(v => v <= pgMajorVersion);
34+
const match = availableVersions.find(entry => entry.version <= pgMajorVersion);
3135

32-
if (matchingVersion === undefined) {
36+
if (match === undefined) {
3337
throw new Error(
3438
`No compatible SQL version for metric "${metricName}" with PostgreSQL ${pgMajorVersion}. ` +
35-
`Available versions: ${availableVersions.join(", ")}`
39+
`Available versions: ${availableVersions.map(entry => entry.key).join(", ")}`
3640
);
3741
}
3842

39-
return metric.sqls[matchingVersion];
43+
const sql = (metric.sqls as unknown as Record<string, string>)[match.key];
44+
45+
if (!sql) {
46+
throw new Error(
47+
`Metric "${metricName}" has no SQL under version key "${match.key}"`
48+
);
49+
}
50+
51+
return sql;
4052
}
4153

4254
/**
@@ -95,6 +107,25 @@ export const METRIC_NAMES = {
95107
I001: "pg_stat_io",
96108
} as const;
97109

110+
/** Schemas whose objects belong to PostgreSQL itself, not to the user. */
111+
const SYSTEM_SCHEMAS = new Set(["pg_catalog", "information_schema", "pg_toast"]);
112+
113+
/**
114+
* True for a PostgreSQL-owned schema (#345).
115+
* Catalog and temp indexes cannot be dropped, so index-health checks must
116+
* never report them. The metric SQL is the primary filter; applying this in
117+
* the getters keeps the report JSON clean if a future SQL edit re-admits a
118+
* system schema.
119+
*/
120+
export function isSystemSchema(name: unknown): boolean {
121+
const schema = String(name ?? "");
122+
return (
123+
SYSTEM_SCHEMAS.has(schema) ||
124+
schema.startsWith("pg_temp_") ||
125+
schema.startsWith("pg_toast_temp_")
126+
);
127+
}
128+
98129
/**
99130
* Transform a row from metrics query output to JSON report format.
100131
* Metrics use `tag_` prefix for dimensions; we strip it for JSON reports.

0 commit comments

Comments
 (0)