@@ -109,6 +109,12 @@ export interface ClusterMetric {
109109
110110/**
111111 * Invalid index entry (H001) - matches H001.schema.json invalidIndex
112+ *
113+ * Decision tree for remediation recommendations:
114+ * 1. has_valid_duplicate=true → DROP (valid duplicate exists, safe to remove)
115+ * 2. is_pk=true or is_unique=true → RECREATE (backs a constraint, must restore)
116+ * 3. table_row_estimate < 10000 → RECREATE (small table, quick rebuild)
117+ * 4. Otherwise → UNCERTAIN (needs manual analysis of query plans)
112118 */
113119export interface InvalidIndex {
114120 schema_name : string ;
@@ -117,9 +123,61 @@ export interface InvalidIndex {
117123 relation_name : string ;
118124 index_size_bytes : number ;
119125 index_size_pretty : string ;
120- /** Full CREATE INDEX statement from pg_get_indexdef(), useful for DROP/CREATE migrations */
126+ /** Full CREATE INDEX statement from pg_get_indexdef() - useful for DROP/RECREATE migrations */
121127 index_definition : string ;
122128 supports_fk : boolean ;
129+ /** True if this index backs a PRIMARY KEY constraint */
130+ is_pk : boolean ;
131+ /** True if this is a UNIQUE index (includes PK indexes) */
132+ is_unique : boolean ;
133+ /** Name of the constraint this index backs, or null if none */
134+ constraint_name : string | null ;
135+ /** Estimated row count of the table from pg_class.reltuples */
136+ table_row_estimate : number ;
137+ /** True if there is a valid index on the same column(s) */
138+ has_valid_duplicate : boolean ;
139+ /** Name of the valid duplicate index if one exists */
140+ valid_duplicate_name : string | null ;
141+ /** Full CREATE INDEX statement of the valid duplicate index */
142+ valid_duplicate_definition : string | null ;
143+ }
144+
145+ /** Recommendation for handling an invalid index */
146+ export type InvalidIndexRecommendation = "DROP" | "RECREATE" | "UNCERTAIN" ;
147+
148+ /** Threshold for considering a table "small" (quick to rebuild) */
149+ const SMALL_TABLE_ROW_THRESHOLD = 10000 ;
150+
151+ /**
152+ * Compute remediation recommendation for an invalid index using decision tree.
153+ *
154+ * Decision tree logic:
155+ * 1. If has_valid_duplicate is true → DROP (valid duplicate exists, safe to remove)
156+ * 2. If is_pk or is_unique is true → RECREATE (backs a constraint, must restore)
157+ * 3. If table_row_estimate < 10000 → RECREATE (small table, quick rebuild)
158+ * 4. Otherwise → UNCERTAIN (needs manual analysis of query plans)
159+ *
160+ * @param index - Invalid index with observation data
161+ * @returns Recommendation: "DROP", "RECREATE", or "UNCERTAIN"
162+ */
163+ export function getInvalidIndexRecommendation ( index : InvalidIndex ) : InvalidIndexRecommendation {
164+ // 1. Valid duplicate exists - safe to drop
165+ if ( index . has_valid_duplicate ) {
166+ return "DROP" ;
167+ }
168+
169+ // 2. Backs a constraint - must recreate
170+ if ( index . is_pk || index . is_unique ) {
171+ return "RECREATE" ;
172+ }
173+
174+ // 3. Small table - quick to recreate
175+ if ( index . table_row_estimate < SMALL_TABLE_ROW_THRESHOLD ) {
176+ return "RECREATE" ;
177+ }
178+
179+ // 4. Large table without clear path - needs manual analysis
180+ return "UNCERTAIN" ;
123181}
124182
125183/**
@@ -564,18 +622,19 @@ export async function getClusterInfo(client: Client, pgMajorVersion: number = 16
564622
565623/**
566624 * Get invalid indexes from the database (H001).
567- * Invalid indexes are indexes that failed to build (e.g., due to CONCURRENTLY failure) .
625+ * Invalid indexes have indisvalid = false, typically from failed CREATE INDEX CONCURRENTLY.
568626 *
569627 * @param client - Connected PostgreSQL client
570628 * @param pgMajorVersion - PostgreSQL major version (default: 16)
571- * @returns Array of invalid index entries with size and FK support info
629+ * @returns Array of invalid index entries with observation data for decision tree analysis
572630 */
573631export async function getInvalidIndexes ( client : Client , pgMajorVersion : number = 16 ) : Promise < InvalidIndex [ ] > {
574632 const sql = getMetricSql ( METRIC_NAMES . H001 , pgMajorVersion ) ;
575633 const result = await client . query ( sql ) ;
576634 return result . rows . map ( ( row ) => {
577635 const transformed = transformMetricRow ( row ) ;
578636 const indexSizeBytes = parseInt ( String ( transformed . index_size_bytes || 0 ) , 10 ) ;
637+
579638 return {
580639 schema_name : String ( transformed . schema_name || "" ) ,
581640 table_name : String ( transformed . table_name || "" ) ,
@@ -585,6 +644,13 @@ export async function getInvalidIndexes(client: Client, pgMajorVersion: number =
585644 index_size_pretty : formatBytes ( indexSizeBytes ) ,
586645 index_definition : String ( transformed . index_definition || "" ) ,
587646 supports_fk : toBool ( transformed . supports_fk ) ,
647+ is_pk : toBool ( transformed . is_pk ) ,
648+ is_unique : toBool ( transformed . is_unique ) ,
649+ constraint_name : transformed . constraint_name ? String ( transformed . constraint_name ) : null ,
650+ table_row_estimate : parseInt ( String ( transformed . table_row_estimate || 0 ) , 10 ) ,
651+ has_valid_duplicate : toBool ( transformed . has_valid_duplicate ) ,
652+ valid_duplicate_name : transformed . valid_index_name ? String ( transformed . valid_index_name ) : null ,
653+ valid_duplicate_definition : transformed . valid_index_definition ? String ( transformed . valid_index_definition ) : null ,
588654 } ;
589655 } ) ;
590656}
0 commit comments