Skip to content

Commit cd793b6

Browse files
author
Dementii Priadko
committed
Merge branch 'claude/add-index-definition-to-h001' into 'main'
feat(reporter): add index_definition to H001 invalid indexes report See merge request postgres-ai/postgres_ai!134
2 parents cc4cd59 + 0a80369 commit cd793b6

9 files changed

Lines changed: 65 additions & 6 deletions

File tree

cli/lib/checkup.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ export interface InvalidIndex {
117117
relation_name: string;
118118
index_size_bytes: number;
119119
index_size_pretty: string;
120+
/** Full CREATE INDEX statement from pg_get_indexdef(), useful for DROP/CREATE migrations */
121+
index_definition: string;
120122
supports_fk: boolean;
121123
}
122124

@@ -581,6 +583,7 @@ export async function getInvalidIndexes(client: Client, pgMajorVersion: number =
581583
relation_name: String(transformed.relation_name || ""),
582584
index_size_bytes: indexSizeBytes,
583585
index_size_pretty: formatBytes(indexSizeBytes),
586+
index_definition: String(transformed.index_definition || ""),
584587
supports_fk: toBool(transformed.supports_fk),
585588
};
586589
});

cli/test/checkup.integration.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,52 @@ describe.skipIf(!!skipReason)("checkup integration: express mode schema compatib
253253
expect(typeof nodeResult.data).toBe("object");
254254
});
255255

256+
test("H001 returns index_definition with CREATE INDEX statement", async () => {
257+
// Create a table and an index, then mark the index as invalid
258+
await client.query(`
259+
CREATE TABLE IF NOT EXISTS test_invalid_idx_table (id serial PRIMARY KEY, value text);
260+
CREATE INDEX IF NOT EXISTS test_invalid_idx ON test_invalid_idx_table(value);
261+
`);
262+
263+
// Mark the index as invalid (simulating a failed CONCURRENTLY build)
264+
await client.query(`
265+
UPDATE pg_index SET indisvalid = false
266+
WHERE indexrelid = 'test_invalid_idx'::regclass;
267+
`);
268+
269+
try {
270+
const report = await checkup.generateH001(client, "test-node");
271+
validateAgainstSchema(report, "H001");
272+
273+
const nodeResult = report.results["test-node"];
274+
const dbName = Object.keys(nodeResult.data)[0];
275+
expect(dbName).toBeTruthy();
276+
277+
const dbData = nodeResult.data[dbName] as any;
278+
expect(dbData.invalid_indexes).toBeDefined();
279+
expect(dbData.invalid_indexes.length).toBeGreaterThan(0);
280+
281+
// Find our test index
282+
const testIndex = dbData.invalid_indexes.find(
283+
(idx: any) => idx.index_name === "test_invalid_idx"
284+
);
285+
expect(testIndex).toBeDefined();
286+
287+
// Verify index_definition contains the actual CREATE INDEX statement
288+
expect(testIndex.index_definition).toMatch(/^CREATE INDEX/);
289+
expect(testIndex.index_definition).toContain("test_invalid_idx");
290+
expect(testIndex.index_definition).toContain("test_invalid_idx_table");
291+
} finally {
292+
// Cleanup: restore the index and drop test objects
293+
await client.query(`
294+
UPDATE pg_index SET indisvalid = true
295+
WHERE indexrelid = 'test_invalid_idx'::regclass;
296+
DROP INDEX IF EXISTS test_invalid_idx;
297+
DROP TABLE IF EXISTS test_invalid_idx_table;
298+
`);
299+
}
300+
});
301+
256302
test("H002 (unused indexes) has correct data structure", async () => {
257303
const report = await checkup.generateH002(client, "test-node");
258304
validateAgainstSchema(report, "H002");

cli/test/checkup.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ describe("H001 - Invalid indexes", () => {
480480
test("getInvalidIndexes returns invalid indexes", async () => {
481481
const mockClient = createMockClient({
482482
invalidIndexesRows: [
483-
{ schema_name: "public", table_name: "users", index_name: "users_email_idx", relation_name: "users", index_size_bytes: "1048576", supports_fk: false },
483+
{ schema_name: "public", table_name: "users", index_name: "users_email_idx", relation_name: "users", index_size_bytes: "1048576", index_definition: "CREATE INDEX users_email_idx ON public.users USING btree (email)", supports_fk: false },
484484
],
485485
});
486486

@@ -491,6 +491,7 @@ describe("H001 - Invalid indexes", () => {
491491
expect(indexes[0].index_name).toBe("users_email_idx");
492492
expect(indexes[0].index_size_bytes).toBe(1048576);
493493
expect(indexes[0].index_size_pretty).toBeTruthy();
494+
expect(indexes[0].index_definition).toMatch(/^CREATE INDEX/);
494495
expect(indexes[0].relation_name).toBe("users");
495496
expect(indexes[0].supports_fk).toBe(false);
496497
});
@@ -502,7 +503,7 @@ describe("H001 - Invalid indexes", () => {
502503
{ name: "server_version_num", setting: "160003" },
503504
],
504505
invalidIndexesRows: [
505-
{ schema_name: "public", table_name: "orders", index_name: "orders_status_idx", relation_name: "orders", index_size_bytes: "2097152", supports_fk: false },
506+
{ schema_name: "public", table_name: "orders", index_name: "orders_status_idx", relation_name: "orders", index_size_bytes: "2097152", index_definition: "CREATE INDEX orders_status_idx ON public.orders USING btree (status)", supports_fk: false },
506507
],
507508
}
508509
);

cli/test/schema-validation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const indexTestData = {
3030
emptyRows: { invalidIndexesRows: [] },
3131
dataRows: {
3232
invalidIndexesRows: [
33-
{ schema_name: "public", table_name: "users", index_name: "users_email_idx", relation_name: "users", index_size_bytes: "1048576", supports_fk: false },
33+
{ schema_name: "public", table_name: "users", index_name: "users_email_idx", relation_name: "users", index_size_bytes: "1048576", index_definition: "CREATE INDEX users_email_idx ON public.users USING btree (email)", supports_fk: false },
3434
],
3535
},
3636
},

config/pgwatch-prometheus/metrics.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,7 @@ metrics:
16041604
quote_ident(pci.relname) as tag_index_name,
16051605
quote_ident(pct.relname) as tag_table_name,
16061606
coalesce(nullif(quote_ident(pn.nspname), 'public') || '.', '') || quote_ident(pct.relname) as tag_relation_name,
1607+
pg_get_indexdef(pidx.indexrelid) as index_definition,
16071608
pg_relation_size(pidx.indexrelid) index_size_bytes,
16081609
((
16091610
select count(1)

reporter/postgres_reports.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,8 @@ def generate_h001_invalid_indexes_report(self, cluster: str = "local", node_name
622622

623623
invalid_indexes_by_db = {}
624624
for db_name in databases:
625+
# Fetch index definitions from the sink for this database (used to aid remediation)
626+
index_definitions = self.get_index_definitions_from_sink(db_name)
625627
# Query invalid indexes for each database
626628
invalid_indexes_query = f'last_over_time(pgwatch_pg_invalid_indexes{{cluster="{cluster}", node_name="{node_name}", datname="{db_name}"}}[3h])'
627629
result = self.query_instant(invalid_indexes_query)
@@ -648,6 +650,7 @@ def generate_h001_invalid_indexes_report(self, cluster: str = "local", node_name
648650
"relation_name": relation_name,
649651
"index_size_bytes": index_size_bytes,
650652
"index_size_pretty": self.format_bytes(index_size_bytes),
653+
"index_definition": index_definitions.get(index_name, "Definition not available"),
651654
"supports_fk": bool(int(supports_fk))
652655
}
653656

@@ -755,7 +758,7 @@ def generate_h002_unused_indexes_report(self, cluster: str = "local", node_name:
755758
'result') else 0
756759

757760
# Get index definition from collected metrics
758-
index_definition = index_definitions.get(index_name, 'Definition not available')
761+
index_definition = index_definitions.get(index_name, "Definition not available")
759762

760763
index_data = {
761764
"schema_name": schema_name,
@@ -885,7 +888,7 @@ def generate_h004_redundant_indexes_report(self, cluster: str = "local", node_na
885888
for idx_name in [r.strip() for r in reason.split(',') if r.strip()]:
886889
redundant_to.append({
887890
"index_name": idx_name,
888-
"index_definition": index_definitions.get(idx_name, 'Definition not available'),
891+
"index_definition": index_definitions.get(idx_name, "Definition not available"),
889892
"index_size_bytes": 0,
890893
"index_size_pretty": "N/A"
891894
})
@@ -901,7 +904,7 @@ def generate_h004_redundant_indexes_report(self, cluster: str = "local", node_na
901904
"table_size_bytes": table_size_bytes,
902905
"index_usage": index_usage,
903906
"supports_fk": supports_fk,
904-
"index_definition": index_definitions.get(index_name, 'Definition not available'),
907+
"index_definition": index_definitions.get(index_name, "Definition not available"),
905908
"index_size_pretty": self.format_bytes(index_size_bytes),
906909
"table_size_pretty": self.format_bytes(table_size_bytes),
907910
"redundant_to": redundant_to

reporter/schemas/H001.schema.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"relation_name",
5050
"index_size_bytes",
5151
"index_size_pretty",
52+
"index_definition",
5253
"supports_fk"
5354
],
5455
"properties": {
@@ -58,6 +59,7 @@
5859
"relation_name": { "type": "string" },
5960
"index_size_bytes": { "type": "number" },
6061
"index_size_pretty": { "type": "string" },
62+
"index_definition": { "type": "string", "description": "Full CREATE INDEX statement from pg_get_indexdef(), useful for DROP/CREATE migrations" },
6163
"supports_fk": { "type": "boolean" }
6264
}
6365
},

tests/reporter/test_generators_unit.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ def test_generate_h001_invalid_indexes_report(
467467
prom_result,
468468
) -> None:
469469
monkeypatch.setattr(generator, "get_all_databases", lambda *args, **kwargs: ["maindb"])
470+
monkeypatch.setattr(generator, "get_index_definitions_from_sink", lambda db: {"idx_invalid": "CREATE INDEX idx_invalid ON public.tbl USING btree (col)"})
470471

471472
responses = {
472473
"pgwatch_pg_invalid_indexes": prom_result(
@@ -494,6 +495,7 @@ def test_generate_h001_invalid_indexes_report(
494495
entry = db_data["invalid_indexes"][0]
495496
assert entry["index_name"] == "idx_invalid"
496497
assert entry["index_size_pretty"].endswith("KiB")
498+
assert entry["index_definition"].startswith("CREATE INDEX")
497499
assert entry["supports_fk"] is True
498500

499501

tests/reporter/test_report_schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ def test_schema_h001(
358358
) -> None:
359359
monkeypatch.setattr(generator, "_get_postgres_version_info", lambda *args, **kwargs: fixed_pg_version)
360360
monkeypatch.setattr(generator, "get_all_databases", lambda *args, **kwargs: ["maindb"])
361+
monkeypatch.setattr(generator, "get_index_definitions_from_sink", lambda db: {"idx_invalid": "CREATE INDEX idx_invalid ON public.tbl USING btree (col)"})
361362
responses = {
362363
"pgwatch_db_size_size_b": prom_result([{"metric": {"datname": "maindb"}, "value": [0, "8192"]}]),
363364
"pgwatch_pg_invalid_indexes": prom_result(

0 commit comments

Comments
 (0)