Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 114 additions & 14 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func runDoctor(ctx context.Context, cfg config.Config, stdout io.Writer, args []
fs := flag.NewFlagSet("doctor", flag.ContinueOnError)
fs.SetOutput(io.Discard)
fix := fs.Bool("fix", false, "apply non-destructive fixes")
deep := fs.Bool("deep", false, "run deeper (potentially slow) diagnostics, including integrity_check")
repoRootFlag := fs.String("repo-root", "", "repository root to inspect (optional)")

defaultRepoRoot := ""
Expand All @@ -151,7 +152,7 @@ func runDoctor(ctx context.Context, cfg config.Config, stdout io.Writer, args []
dbPath = p
}

report, err := doctor.RunWithFix(*fix, dbPath)
report, err := doctor.RunWithOptions(doctor.Options{Fix: *fix, DBPath: dbPath, Deep: *deep})
if err != nil {
return err
}
Expand Down Expand Up @@ -1063,6 +1064,9 @@ func runClean(ctx context.Context, cfg config.Config, stdout io.Writer, args []s
fs.SetOutput(io.Discard)
vacuum := fs.Bool("vacuum", false, "run VACUUM on databases")
ftsOptimize := fs.Bool("fts-optimize", false, "run FTS optimize on databases (symbol_fts)")
analyze := fs.Bool("analyze", false, "run ANALYZE on databases")
walCheckpointTruncate := fs.Bool("wal-checkpoint-truncate", false, "run PRAGMA wal_checkpoint(TRUNCATE) on databases")
incrementalVacuum := fs.Bool("incremental-vacuum", false, "run PRAGMA incremental_vacuum (requires auto_vacuum=INCREMENTAL)")
repoRootFlag := fs.String("repo-root", "", "repository root to clean")

defaultRepoRoot := ""
Expand All @@ -1075,25 +1079,64 @@ func runClean(ctx context.Context, cfg config.Config, stdout io.Writer, args []s
}

type dbResult struct {
Path string `json:"path"`
Action string `json:"action"`
SizeBefore int64 `json:"size_before_bytes"`
SizeAfter int64 `json:"size_after_bytes"`
ReclaimedBytes int64 `json:"reclaimed_bytes"`
Vacuumed bool `json:"vacuumed,omitempty"`
FTSOptimized bool `json:"fts_optimized,omitempty"`
FTSOptimizeMS int64 `json:"fts_optimize_ms,omitempty"`
CanonicalRepo string `json:"canonical_repo,omitempty"`
Error string `json:"error,omitempty"`
Path string `json:"path"`
Action string `json:"action"`
SizeBefore int64 `json:"size_before_bytes"`
SizeAfter int64 `json:"size_after_bytes"`
ReclaimedBytes int64 `json:"reclaimed_bytes"`
Vacuumed bool `json:"vacuumed,omitempty"`
FTSOptimized bool `json:"fts_optimized,omitempty"`
FTSOptimizeMS int64 `json:"fts_optimize_ms,omitempty"`
Analyzed bool `json:"analyzed,omitempty"`
AnalyzeMS int64 `json:"analyze_ms,omitempty"`
WALCheckpoint *store.WalCheckpointResult `json:"wal_checkpoint,omitempty"`
IncVacuumed bool `json:"incremental_vacuumed,omitempty"`
IncVacuumMS int64 `json:"incremental_vacuum_ms,omitempty"`
IncVacuumBeforeFreelist int64 `json:"incremental_vacuum_before_freelist_pages,omitempty"`
IncVacuumAfterFreelist int64 `json:"incremental_vacuum_after_freelist_pages,omitempty"`
CanonicalRepo string `json:"canonical_repo,omitempty"`
Error string `json:"error,omitempty"`
}
report := map[string]any{
"vacuum": *vacuum,
"fts_optimize": *ftsOptimize,
"dbs": []dbResult{},
"vacuum": *vacuum,
"fts_optimize": *ftsOptimize,
"analyze": *analyze,
"wal_checkpoint_truncate": *walCheckpointTruncate,
"incremental_vacuum": *incrementalVacuum,
"dbs": []dbResult{},
}
results := make([]dbResult, 0)
var reclaimed int64

runAnalyze := func(ctx context.Context, s *store.Store, res *dbResult) error {
dur, err := s.Analyze(ctx)
if err != nil {
return err
}
res.Analyzed = true
res.AnalyzeMS = dur.Milliseconds()
return nil
}
runWalCheckpointTruncate := func(ctx context.Context, s *store.Store, res *dbResult) error {
walRes, err := s.WalCheckpointTruncate(ctx)
if err != nil {
return err
}
res.WALCheckpoint = &walRes
return nil
}
runIncrementalVacuumAll := func(ctx context.Context, s *store.Store, res *dbResult) error {
beforePages, afterPages, dur, err := s.IncrementalVacuumAll(ctx)
if err != nil {
return err
}
res.IncVacuumed = true
res.IncVacuumMS = dur.Milliseconds()
res.IncVacuumBeforeFreelist = beforePages
res.IncVacuumAfterFreelist = afterPages
return nil
}

if repoRoot != "" {
canonical, err := store.CanonicalRepoPath(repoRoot)
if err != nil {
Expand All @@ -1111,6 +1154,11 @@ func runClean(ctx context.Context, cfg config.Config, stdout io.Writer, args []s
return err
}
defer s.Close()
if *analyze {
if err := runAnalyze(ctx, s, &res); err != nil {
return err
}
}
Comment thread
isink17 marked this conversation as resolved.
if *ftsOptimize {
dur, err := s.OptimizeFTS(ctx)
if err != nil {
Expand All @@ -1119,12 +1167,28 @@ func runClean(ctx context.Context, cfg config.Config, stdout io.Writer, args []s
res.FTSOptimized = true
res.FTSOptimizeMS = dur.Milliseconds()
}
if *walCheckpointTruncate {
if err := runWalCheckpointTruncate(ctx, s, &res); err != nil {
return err
}
}
if *incrementalVacuum {
if err := runIncrementalVacuumAll(ctx, s, &res); err != nil {
return err
}
}
Comment on lines +1170 to +1179

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for WalCheckpointTruncate and IncrementalVacuumAll is duplicated across the two branches of runClean. This increases the risk of inconsistencies if the logic needs to be updated. Please refactor this into a shared helper function.

if *vacuum {
if err := s.Vacuum(ctx); err != nil {
return err
}
res.Vacuumed = true
res.Action = "vacuumed"
} else if *incrementalVacuum {
res.Action = "incremental_vacuumed"
} else if *walCheckpointTruncate {
res.Action = "wal_checkpointed"
} else if *analyze {
res.Action = "analyzed"
} else if *ftsOptimize {
res.Action = "fts_optimized"
} else {
Expand Down Expand Up @@ -1201,6 +1265,18 @@ func runClean(ctx context.Context, cfg config.Config, stdout io.Writer, args []s
if !*vacuum {
res.Action = "kept"
}
if *analyze {
if err := runAnalyze(ctx, s, &res); err != nil {
_ = s.Close()
res.Action = "skipped"
res.Error = err.Error()
results = append(results, res)
continue
}
if !*vacuum && !*ftsOptimize && res.Action == "kept" {
res.Action = "analyzed"
}
}
if *ftsOptimize {
dur, err := s.OptimizeFTS(ctx)
if err != nil {
Expand All @@ -1216,6 +1292,30 @@ func runClean(ctx context.Context, cfg config.Config, stdout io.Writer, args []s
res.Action = "fts_optimized"
}
}
if *walCheckpointTruncate {
if err := runWalCheckpointTruncate(ctx, s, &res); err != nil {
_ = s.Close()
res.Action = "skipped"
res.Error = err.Error()
results = append(results, res)
continue
}
if !*vacuum && !*ftsOptimize && !*analyze && res.Action == "kept" {
res.Action = "wal_checkpointed"
}
}
if *incrementalVacuum {
if err := runIncrementalVacuumAll(ctx, s, &res); err != nil {
_ = s.Close()
res.Action = "skipped"
res.Error = err.Error()
results = append(results, res)
continue
}
if !*vacuum && !*ftsOptimize && !*analyze && !*walCheckpointTruncate && res.Action == "kept" {
res.Action = "incremental_vacuumed"
}
}
if *vacuum {
if err := s.Vacuum(ctx); err != nil {
_ = s.Close()
Expand Down
8 changes: 7 additions & 1 deletion internal/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ func newCommandList() []*command {
" doctor",
" add --repo-root PATH to inspect a repo DB",
" add --fix for non-destructive autofixes",
" add --deep for slower DB checks (integrity_check)",
},
run: func(ctx context.Context, cfg config.Config, stdout, stderr io.Writer, invokedName string, args []string) error {
return runDoctor(ctx, cfg, stdout, args)
Expand Down Expand Up @@ -325,15 +326,20 @@ func newCommandList() []*command {
{
name: "clean",
description: "clean index data",
usageLines: []string{" clean [repo-path] [--vacuum] [--fts-optimize]"},
usageLines: []string{" clean [repo-path] [--vacuum] [--fts-optimize] [--analyze] [--wal-checkpoint-truncate] [--incremental-vacuum]"},
flags: []commandFlag{
{name: "--vacuum", description: "VACUUM the database after cleanup"},
{name: "--fts-optimize", description: "run FTS optimize (symbol_fts)"},
{name: "--analyze", description: "run ANALYZE"},
{name: "--wal-checkpoint-truncate", description: "run PRAGMA wal_checkpoint(TRUNCATE)"},
{name: "--incremental-vacuum", description: "run PRAGMA incremental_vacuum (requires auto_vacuum=INCREMENTAL)"},
},
examples: []string{
"codegraph clean .",
"codegraph clean . --vacuum",
"codegraph clean . --fts-optimize",
"codegraph clean . --analyze",
"codegraph clean . --wal-checkpoint-truncate",
},
run: func(ctx context.Context, cfg config.Config, stdout, stderr io.Writer, invokedName string, args []string) error {
return runClean(ctx, cfg, stdout, args)
Expand Down
121 changes: 107 additions & 14 deletions internal/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@ import (
)

type Report struct {
GOOS string `json:"goos"`
ConfigPath string `json:"config_path"`
ConfigExists bool `json:"config_exists"`
DataDir string `json:"data_dir"`
CacheDir string `json:"cache_dir"`
CodegraphOnPath bool `json:"codegraph_on_path"`
CodegraphPath string `json:"codegraph_path,omitempty"`
SQLiteDriver string `json:"sqlite_driver"`
DB *DBInfo `json:"db,omitempty"`
AppliedFixes []string `json:"applied_fixes,omitempty"`
Recommendations []string `json:"recommendations,omitempty"`
GOOS string `json:"goos"`
ConfigPath string `json:"config_path"`
ConfigExists bool `json:"config_exists"`
DataDir string `json:"data_dir"`
CacheDir string `json:"cache_dir"`
CodegraphOnPath bool `json:"codegraph_on_path"`
CodegraphPath string `json:"codegraph_path,omitempty"`
SQLiteDriver string `json:"sqlite_driver"`
DB *DBInfo `json:"db,omitempty"`
Deep *DeepInfo `json:"deep,omitempty"`
AppliedFixes []string `json:"applied_fixes,omitempty"`
Recommendations []string `json:"recommendations,omitempty"`
}

type DBInfo struct {
Expand All @@ -38,11 +39,32 @@ type DBInfo struct {
Pragmas store.DBPragmas `json:"pragmas"`
}

type DeepInfo struct {
DB *DBDeepInfo `json:"db,omitempty"`
}

type DBDeepInfo struct {
IntegrityOK bool `json:"integrity_ok"`
IntegrityMessages []string `json:"integrity_messages,omitempty"`
IntegrityTruncated bool `json:"integrity_truncated,omitempty"`
ForeignKeyIssues int64 `json:"foreign_key_issues,omitempty"`
}

func Run() (Report, error) {
return RunWithFix(false, "")
}

func RunWithFix(fix bool, dbPath string) (Report, error) {
return RunWithOptions(Options{Fix: fix, DBPath: dbPath})
}

type Options struct {
Fix bool
DBPath string
Deep bool
}

func RunWithOptions(opts Options) (Report, error) {
paths, err := platform.DefaultPaths()
if err != nil {
return Report{}, err
Expand All @@ -61,7 +83,7 @@ func RunWithFix(fix bool, dbPath string) (Report, error) {
binaryPath = ""
}
appliedFixes := []string{}
if fix {
if opts.Fix {
defaultCfg, err := config.Default()
if err != nil {
return Report{}, err
Expand Down Expand Up @@ -96,13 +118,22 @@ func RunWithFix(fix bool, dbPath string) (Report, error) {
}

var dbInfo *DBInfo
if strings.TrimSpace(dbPath) != "" {
info, err := inspectDB(context.Background(), dbPath)
var deepInfo *DeepInfo
if strings.TrimSpace(opts.DBPath) != "" {
info, err := inspectDB(context.Background(), opts.DBPath)
if err != nil {
recommendations = append(recommendations, "repo DB inspect failed: "+err.Error())
} else {
dbInfo = info
}
if opts.Deep {
di, err := inspectDeepDB(context.Background(), opts.DBPath)
if err != nil {
recommendations = append(recommendations, "repo DB deep inspect failed: "+err.Error())
} else {
deepInfo = &DeepInfo{DB: di}
}
}
}

return Report{
Expand All @@ -115,6 +146,7 @@ func RunWithFix(fix bool, dbPath string) (Report, error) {
CodegraphPath: binaryPath,
SQLiteDriver: store.SQLiteDriverName(),
DB: dbInfo,
Deep: deepInfo,
AppliedFixes: appliedFixes,
Recommendations: recommendations,
}, nil
Expand Down Expand Up @@ -149,3 +181,64 @@ func inspectDB(ctx context.Context, dbPath string) (*DBInfo, error) {
Pragmas: pragmas,
}, nil
}

func inspectDeepDB(ctx context.Context, dbPath string) (*DBDeepInfo, error) {
db, err := sql.Open(store.SQLiteDriverName(), dbPath)
if err != nil {
return nil, err
}
defer db.Close()

// integrity_check can return many rows; keep this bounded and human-usable.
const maxIntegrityMessages = 20
integrityMessages := make([]string, 0)
truncated := false
rows, err := db.QueryContext(ctx, `PRAGMA integrity_check`)
if err != nil {
return nil, err
}
for rows.Next() {
var msg string
if err := rows.Scan(&msg); err != nil {
rows.Close()
return nil, err
}
if len(integrityMessages) < maxIntegrityMessages {
integrityMessages = append(integrityMessages, msg)
} else {
truncated = true
}
}
if err := rows.Err(); err != nil {
rows.Close()
return nil, err
}
rows.Close()

integrityOK := len(integrityMessages) == 1 && integrityMessages[0] == "ok"
if integrityOK {
integrityMessages = nil
truncated = false
}

var foreignKeyIssues int64
fkRows, err := db.QueryContext(ctx, `PRAGMA foreign_key_check`)
if err != nil {
return nil, err
}
for fkRows.Next() {
foreignKeyIssues++
}
if err := fkRows.Err(); err != nil {
fkRows.Close()
return nil, err
}
fkRows.Close()

return &DBDeepInfo{
IntegrityOK: integrityOK,
IntegrityMessages: integrityMessages,
IntegrityTruncated: truncated,
ForeignKeyIssues: foreignKeyIssues,
}, nil
}
Loading
Loading