@@ -182,17 +182,25 @@ type indexCandidate struct {
182182// precedes btree reindex so reclaimed space is reflected in the bloat
183183// measurement that drives the rebuild decision.
184184func (s * Driver ) Optimize (ctx context.Context ) error {
185- if installed , err := s .pgstattupleInstalled (ctx ); err != nil {
185+ return optimize (ctx , s .pool )
186+ }
187+
188+ // optimize is the package-level implementation that the four maintenance
189+ // phases dispatch through. Splitting Optimize from its receiver lets unit
190+ // tests drive the phase wiring against a fake driver without standing up a
191+ // real *pgxpool.Pool; the *Driver method is a forwarder.
192+ func optimize (ctx context.Context , db driver ) error {
193+ if installed , err := pgstattupleInstalled (ctx , db ); err != nil {
186194 return fmt .Errorf ("checking pgstattuple extension: %w" , err )
187195 } else if ! installed {
188196 slog .WarnContext (ctx , "Index optimization skipped: pgstattuple extension is not installed; verify the DAWGS schema bootstrap completed successfully" )
189197 return nil
190198 }
191199
192- s . cleanupOrphanedReindexArtifacts (ctx )
193- s . flushGinPendingLists (ctx )
194- s . vacuumGraphPartitions (ctx )
195- s . reindexBloatedBtreeIndexes (ctx )
200+ cleanupOrphanedReindexArtifacts (ctx , db )
201+ flushGinPendingLists (ctx , db )
202+ vacuumGraphPartitions (ctx , db )
203+ reindexBloatedBtreeIndexes (ctx , db )
196204 return nil
197205}
198206
@@ -216,8 +224,8 @@ func needsReindex(c indexCandidate) (string, bool) {
216224// fatal; the loop honors ctx cancellation between candidates. Candidates are
217225// processed smallest first so that an early cancellation still produces the
218226// maximum number of completed rebuilds.
219- func ( s * Driver ) reindexBloatedBtreeIndexes (ctx context.Context ) {
220- indexes , err := s . listGraphPartitionBtreeIndexes (ctx )
227+ func reindexBloatedBtreeIndexes (ctx context.Context , db driver ) {
228+ indexes , err := listGraphPartitionBtreeIndexes (ctx , db )
221229 if err != nil {
222230 slog .WarnContext (ctx , fmt .Sprintf ("Btree reindex scan failed; continuing: %v" , err ))
223231 return
@@ -230,7 +238,7 @@ func (s *Driver) reindexBloatedBtreeIndexes(ctx context.Context) {
230238 totalBytes int64
231239 )
232240 for _ , idx := range indexes {
233- density , fragmentation , err := s . measureIndexBloat (ctx , idx .oid )
241+ density , fragmentation , err := measureIndexBloat (ctx , db , idx .oid )
234242 if err != nil {
235243 slog .WarnContext (ctx , fmt .Sprintf ("Skipping bloat assessment for index %s.%s: %v" , idx .schema , idx .index , err ))
236244 continue
@@ -258,19 +266,19 @@ func (s *Driver) reindexBloatedBtreeIndexes(ctx context.Context) {
258266 return candidates [i ].sizeBytes < candidates [j ].sizeBytes
259267 })
260268
261- s . reindexCandidates (ctx , candidates )
269+ reindexCandidates (ctx , db , candidates )
262270}
263271
264- func ( s * Driver ) pgstattupleInstalled (ctx context.Context ) (bool , error ) {
272+ func pgstattupleInstalled (ctx context.Context , db driver ) (bool , error ) {
265273 var installed bool
266- if err := s . pool .QueryRow (ctx , sqlPgstattupleInstalled ).Scan (& installed ); err != nil {
274+ if err := db .QueryRow (ctx , sqlPgstattupleInstalled ).Scan (& installed ); err != nil {
267275 return false , err
268276 }
269277 return installed , nil
270278}
271279
272- func ( s * Driver ) listGraphPartitionBtreeIndexes (ctx context.Context ) ([]indexRow , error ) {
273- rows , err := s . pool .Query (ctx , sqlSelectGraphPartitionBtreeIndexes )
280+ func listGraphPartitionBtreeIndexes (ctx context.Context , db driver ) ([]indexRow , error ) {
281+ rows , err := db .Query (ctx , sqlSelectGraphPartitionBtreeIndexes )
274282 if err != nil {
275283 return nil , err
276284 }
@@ -287,9 +295,9 @@ func (s *Driver) listGraphPartitionBtreeIndexes(ctx context.Context) ([]indexRow
287295 return out , rows .Err ()
288296}
289297
290- func ( s * Driver ) measureIndexBloat (ctx context.Context , indexOID uint32 ) (float64 , float64 , error ) {
298+ func measureIndexBloat (ctx context.Context , db driver , indexOID uint32 ) (float64 , float64 , error ) {
291299 var density , fragmentation float64
292- if err := s . pool .QueryRow (ctx , sqlSelectIndexBloatMetrics , indexOID ).Scan (& density , & fragmentation ); err != nil {
300+ if err := db .QueryRow (ctx , sqlSelectIndexBloatMetrics , indexOID ).Scan (& density , & fragmentation ); err != nil {
293301 return 0 , 0 , err
294302 }
295303 return density , fragmentation , nil
@@ -306,8 +314,8 @@ type orphanedReindexArtifact struct {
306314// left behind by previously aborted REINDEX CONCURRENTLY runs. Failures are
307315// logged at WARN and never fatal: an orphan wastes disk but does not block
308316// productive rebuilds, so a stuck cleanup must not gate the rest of the pass.
309- func ( s * Driver ) cleanupOrphanedReindexArtifacts (ctx context.Context ) {
310- orphans , err := s . listOrphanedReindexArtifacts (ctx )
317+ func cleanupOrphanedReindexArtifacts (ctx context.Context , db driver ) {
318+ orphans , err := listOrphanedReindexArtifacts (ctx , db )
311319 if err != nil {
312320 slog .WarnContext (ctx , fmt .Sprintf ("Index optimization cleanup: failed to scan for orphaned reindex artifacts; continuing: %v" , err ))
313321 return
@@ -321,16 +329,16 @@ func (s *Driver) cleanupOrphanedReindexArtifacts(ctx context.Context) {
321329
322330 slog .InfoContext (ctx , fmt .Sprintf ("Orphan reindex cleanup assessment complete: %d artifact(s) to drop" , len (orphans )))
323331 for _ , o := range orphans {
324- if _ , err := s . pool .Exec (ctx , buildDropInvalidIndexSQL (o .schema , o .name )); err != nil {
332+ if _ , err := db .Exec (ctx , buildDropInvalidIndexSQL (o .schema , o .name )); err != nil {
325333 slog .WarnContext (ctx , fmt .Sprintf ("Index optimization cleanup: failed to drop orphaned reindex artifact %s.%s; continuing: %v" , o .schema , o .name , err ))
326334 continue
327335 }
328336 slog .InfoContext (ctx , fmt .Sprintf ("Index optimization cleanup: dropped orphaned reindex artifact %s.%s" , o .schema , o .name ))
329337 }
330338}
331339
332- func ( s * Driver ) listOrphanedReindexArtifacts (ctx context.Context ) ([]orphanedReindexArtifact , error ) {
333- rows , err := s . pool .Query (ctx , sqlSelectOrphanedReindexArtifacts )
340+ func listOrphanedReindexArtifacts (ctx context.Context , db driver ) ([]orphanedReindexArtifact , error ) {
341+ rows , err := db .Query (ctx , sqlSelectOrphanedReindexArtifacts )
334342 if err != nil {
335343 return nil , err
336344 }
@@ -352,7 +360,7 @@ func (s *Driver) listOrphanedReindexArtifacts(ctx context.Context) ([]orphanedRe
352360// Per-candidate failures are logged at WARN and the loop continues; ctx
353361// cancellation aborts further candidates but in-flight REINDEX statements
354362// must run to completion in Postgres to avoid leaving _ccnew artifacts.
355- func ( s * Driver ) reindexCandidates (ctx context.Context , candidates []indexCandidate ) {
363+ func reindexCandidates (ctx context.Context , db driver , candidates []indexCandidate ) {
356364 for _ , c := range candidates {
357365 if err := ctx .Err (); err != nil {
358366 slog .WarnContext (ctx , fmt .Sprintf ("Index optimization rebuild cancelled before processing %s.%s: %v" , c .schema , c .index , err ))
@@ -365,7 +373,7 @@ func (s *Driver) reindexCandidates(ctx context.Context, candidates []indexCandid
365373 ))
366374
367375 started := time .Now ()
368- if _ , err := s . pool .Exec (ctx , buildReindexSQL (c .schema , c .index )); err != nil {
376+ if _ , err := db .Exec (ctx , buildReindexSQL (c .schema , c .index )); err != nil {
369377 slog .WarnContext (ctx , fmt .Sprintf (
370378 "Index optimization rebuild failed for %s.%s after %s; continuing with next candidate: %v" ,
371379 c .schema , c .index , time .Since (started ), err ,
@@ -432,8 +440,8 @@ func needsGinFlush(c ginFlushCandidate) (string, bool) {
432440// threshold. Per-candidate failures are logged at WARN and never fatal: a
433441// stuck flush wastes time on the next pass but does not block the rest of
434442// the optimization phases.
435- func ( s * Driver ) flushGinPendingLists (ctx context.Context ) {
436- indexes , err := s . listGraphPartitionGinIndexes (ctx )
443+ func flushGinPendingLists (ctx context.Context , db driver ) {
444+ indexes , err := listGraphPartitionGinIndexes (ctx , db )
437445 if err != nil {
438446 slog .WarnContext (ctx , fmt .Sprintf ("Index optimization GIN scan failed; continuing: %v" , err ))
439447 return
@@ -446,7 +454,7 @@ func (s *Driver) flushGinPendingLists(ctx context.Context) {
446454 totalPendingPages int64
447455 )
448456 for _ , idx := range indexes {
449- pages , tuples , err := s . measureGinPending (ctx , idx .oid )
457+ pages , tuples , err := measureGinPending (ctx , db , idx .oid )
450458 if err != nil {
451459 slog .WarnContext (ctx , fmt .Sprintf ("Skipping GIN pending-list assessment for index %s.%s: %v" , idx .schema , idx .index , err ))
452460 continue
@@ -480,7 +488,7 @@ func (s *Driver) flushGinPendingLists(ctx context.Context) {
480488 }
481489
482490 started := time .Now ()
483- if _ , err := s . pool .Exec (ctx , sqlCleanGinPendingList , c .oid ); err != nil {
491+ if _ , err := db .Exec (ctx , sqlCleanGinPendingList , c .oid ); err != nil {
484492 slog .WarnContext (ctx , fmt .Sprintf (
485493 "GIN flush failed for %s.%s after %s; continuing with next candidate: %v" ,
486494 c .schema , c .index , time .Since (started ), err ,
@@ -495,8 +503,8 @@ func (s *Driver) flushGinPendingLists(ctx context.Context) {
495503 }
496504}
497505
498- func ( s * Driver ) listGraphPartitionGinIndexes (ctx context.Context ) ([]ginIndexRow , error ) {
499- rows , err := s . pool .Query (ctx , sqlSelectGraphPartitionGinIndexes )
506+ func listGraphPartitionGinIndexes (ctx context.Context , db driver ) ([]ginIndexRow , error ) {
507+ rows , err := db .Query (ctx , sqlSelectGraphPartitionGinIndexes )
500508 if err != nil {
501509 return nil , err
502510 }
@@ -513,9 +521,9 @@ func (s *Driver) listGraphPartitionGinIndexes(ctx context.Context) ([]ginIndexRo
513521 return out , rows .Err ()
514522}
515523
516- func ( s * Driver ) measureGinPending (ctx context.Context , indexOID uint32 ) (int64 , int64 , error ) {
524+ func measureGinPending (ctx context.Context , db driver , indexOID uint32 ) (int64 , int64 , error ) {
517525 var pendingPages , pendingTuples int64
518- if err := s . pool .QueryRow (ctx , sqlSelectGinPendingMetrics , indexOID ).Scan (& pendingPages , & pendingTuples ); err != nil {
526+ if err := db .QueryRow (ctx , sqlSelectGinPendingMetrics , indexOID ).Scan (& pendingPages , & pendingTuples ); err != nil {
519527 return 0 , 0 , err
520528 }
521529 return pendingPages , pendingTuples , nil
@@ -592,8 +600,8 @@ func vacuumAssessment(r vacuumStatsRow, now time.Time) (vacuumCandidate, bool) {
592600// flagged by vacuumAssessment. Per-candidate failures are logged at WARN and
593601// never fatal. The loop honors ctx cancellation between partitions; an
594602// in-flight VACUUM aborts at the next safe point when ctx is cancelled.
595- func ( s * Driver ) vacuumGraphPartitions (ctx context.Context ) {
596- stats , err := s . listGraphPartitionVacuumStats (ctx )
603+ func vacuumGraphPartitions (ctx context.Context , db driver ) {
604+ stats , err := listGraphPartitionVacuumStats (ctx , db )
597605 if err != nil {
598606 slog .WarnContext (ctx , fmt .Sprintf ("Vacuum assessment scan failed; continuing: %v" , err ))
599607 return
@@ -639,7 +647,7 @@ func (s *Driver) vacuumGraphPartitions(ctx context.Context) {
639647 }
640648
641649 started := time .Now ()
642- if _ , err := s . pool .Exec (ctx , buildVacuumSQL (c .schema , c .table )); err != nil {
650+ if _ , err := db .Exec (ctx , buildVacuumSQL (c .schema , c .table )); err != nil {
643651 slog .WarnContext (ctx , fmt .Sprintf (
644652 "Vacuum failed for %s.%s after %s; continuing with next candidate: %v" ,
645653 c .schema , c .table , time .Since (started ), err ,
@@ -654,8 +662,8 @@ func (s *Driver) vacuumGraphPartitions(ctx context.Context) {
654662 }
655663}
656664
657- func ( s * Driver ) listGraphPartitionVacuumStats (ctx context.Context ) ([]vacuumStatsRow , error ) {
658- rows , err := s . pool .Query (ctx , sqlSelectGraphPartitionVacuumStats )
665+ func listGraphPartitionVacuumStats (ctx context.Context , db driver ) ([]vacuumStatsRow , error ) {
666+ rows , err := db .Query (ctx , sqlSelectGraphPartitionVacuumStats )
659667 if err != nil {
660668 return nil , err
661669 }
0 commit comments