328 lines
9.2 KiB
Go
328 lines
9.2 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// OpenDB opens or creates the SQLite database and applies the schema.
|
|
func OpenDB(dbPath, schemaPath string) (*sql.DB, error) {
|
|
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=ON")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening database: %w", err)
|
|
}
|
|
|
|
schema, err := os.ReadFile(schemaPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("reading schema: %w", err)
|
|
}
|
|
|
|
if _, err := db.Exec(string(schema)); err != nil {
|
|
return nil, fmt.Errorf("applying schema: %w", err)
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// DBWriter writes analysis results to the SQLite database.
|
|
type DBWriter struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewDBWriter creates a new DBWriter.
|
|
func NewDBWriter(db *sql.DB) *DBWriter {
|
|
return &DBWriter{db: db}
|
|
}
|
|
|
|
// WriteAll writes all analysis results to the database in a single transaction.
|
|
func (w *DBWriter) WriteAll(result *AnalysisResult) error {
|
|
tx, err := w.db.Begin()
|
|
if err != nil {
|
|
return fmt.Errorf("beginning transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
moduleIDs := make(map[string]int64)
|
|
featureIDs := make(map[string]int64)
|
|
|
|
for _, mod := range result.Modules {
|
|
modID, err := w.insertModule(tx, &mod)
|
|
if err != nil {
|
|
return fmt.Errorf("inserting module %s: %w", mod.Name, err)
|
|
}
|
|
moduleIDs[mod.Name] = modID
|
|
|
|
for _, feat := range mod.Features {
|
|
featID, err := w.insertFeature(tx, modID, &feat)
|
|
if err != nil {
|
|
return fmt.Errorf("inserting feature %s: %w", feat.Name, err)
|
|
}
|
|
featureIDs[mod.Name+":"+feat.Name] = featID
|
|
}
|
|
|
|
for _, test := range mod.Tests {
|
|
var featureID *int64
|
|
if test.FeatureName != "" {
|
|
if fid, ok := featureIDs[mod.Name+":"+test.FeatureName]; ok {
|
|
featureID = &fid
|
|
}
|
|
}
|
|
if err := w.insertTest(tx, modID, featureID, &test); err != nil {
|
|
return fmt.Errorf("inserting test %s: %w", test.Name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, dep := range result.Dependencies {
|
|
sourceID, ok := moduleIDs[dep.SourceModule]
|
|
if !ok {
|
|
continue
|
|
}
|
|
targetID, ok := moduleIDs[dep.TargetModule]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if err := w.insertDependency(tx, "module", sourceID, "module", targetID, dep.DependencyKind); err != nil {
|
|
return fmt.Errorf("inserting dependency %s->%s: %w", dep.SourceModule, dep.TargetModule, err)
|
|
}
|
|
}
|
|
|
|
for _, imp := range result.Imports {
|
|
if imp.IsStdlib {
|
|
continue
|
|
}
|
|
if err := w.insertLibrary(tx, &imp); err != nil {
|
|
return fmt.Errorf("inserting library %s: %w", imp.ImportPath, err)
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
func (w *DBWriter) insertModule(tx *sql.Tx, mod *Module) (int64, error) {
|
|
res, err := tx.Exec(
|
|
`INSERT INTO modules (name, description, go_package, go_file, go_line_count, status)
|
|
VALUES (?, ?, ?, ?, ?, 'not_started')`,
|
|
mod.Name, mod.Description, mod.GoPackage, mod.GoFile, mod.GoLineCount,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (w *DBWriter) insertFeature(tx *sql.Tx, moduleID int64, feat *Feature) (int64, error) {
|
|
res, err := tx.Exec(
|
|
`INSERT INTO features (module_id, name, description, go_file, go_class, go_method, go_line_number, go_line_count, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'not_started')`,
|
|
moduleID, feat.Name, feat.Description, feat.GoFile, feat.GoClass, feat.GoMethod, feat.GoLineNumber, feat.GoLineCount,
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return res.LastInsertId()
|
|
}
|
|
|
|
func (w *DBWriter) insertTest(tx *sql.Tx, moduleID int64, featureID *int64, test *TestFunc) error {
|
|
_, err := tx.Exec(
|
|
`INSERT INTO unit_tests (module_id, feature_id, name, description, go_file, go_class, go_method, go_line_number, go_line_count, status)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'not_started')`,
|
|
moduleID, featureID, test.Name, test.Description, test.GoFile, test.GoClass, test.GoMethod, test.GoLineNumber, test.GoLineCount,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (w *DBWriter) insertDependency(tx *sql.Tx, srcType string, srcID int64, tgtType string, tgtID int64, kind string) error {
|
|
_, err := tx.Exec(
|
|
`INSERT OR IGNORE INTO dependencies (source_type, source_id, target_type, target_id, dependency_kind)
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
srcType, srcID, tgtType, tgtID, kind,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (w *DBWriter) insertLibrary(tx *sql.Tx, imp *ImportInfo) error {
|
|
_, err := tx.Exec(
|
|
`INSERT OR IGNORE INTO library_mappings (go_import_path, go_library_name, status)
|
|
VALUES (?, ?, 'not_mapped')`,
|
|
imp.ImportPath, imp.ImportPath,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// OpenDBNoSchema opens an existing SQLite database without applying schema.
|
|
// It verifies that the required tables exist.
|
|
func OpenDBNoSchema(dbPath string) (*sql.DB, error) {
|
|
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_foreign_keys=ON")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("opening database: %w", err)
|
|
}
|
|
|
|
// Verify required tables exist
|
|
for _, table := range []string{"modules", "features", "unit_tests", "dependencies"} {
|
|
var name string
|
|
err := db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&name)
|
|
if err != nil {
|
|
db.Close()
|
|
return nil, fmt.Errorf("required table %q not found: %w", table, err)
|
|
}
|
|
}
|
|
|
|
return db, nil
|
|
}
|
|
|
|
// CallGraphStats holds summary statistics from a call-graph update.
|
|
type CallGraphStats struct {
|
|
TestsAnalyzed int
|
|
TestsLinked int
|
|
DependencyRows int
|
|
FeatureIDsSet int
|
|
}
|
|
|
|
// UpdateCallGraph writes call-graph analysis results to the database incrementally.
|
|
func (w *DBWriter) UpdateCallGraph(result *AnalysisResult) (*CallGraphStats, error) {
|
|
stats := &CallGraphStats{}
|
|
|
|
// Load module name→ID mapping
|
|
moduleIDs := make(map[string]int64)
|
|
rows, err := w.db.Query("SELECT id, name FROM modules")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying modules: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var id int64
|
|
var name string
|
|
if err := rows.Scan(&id, &name); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
moduleIDs[name] = id
|
|
}
|
|
rows.Close()
|
|
|
|
// Load feature DB IDs: "module_name:go_method:go_class" → id
|
|
type featureKey struct {
|
|
moduleName string
|
|
goMethod string
|
|
goClass string
|
|
}
|
|
featureDBIDs := make(map[featureKey]int64)
|
|
rows, err = w.db.Query(`
|
|
SELECT f.id, m.name, f.go_method, COALESCE(f.go_class, '')
|
|
FROM features f
|
|
JOIN modules m ON f.module_id = m.id
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying features: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var id int64
|
|
var modName, goMethod, goClass string
|
|
if err := rows.Scan(&id, &modName, &goMethod, &goClass); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
featureDBIDs[featureKey{modName, goMethod, goClass}] = id
|
|
}
|
|
rows.Close()
|
|
|
|
// Load test DB IDs: "module_name:go_method" → id
|
|
testDBIDs := make(map[string]int64)
|
|
rows, err = w.db.Query(`
|
|
SELECT ut.id, m.name, ut.go_method
|
|
FROM unit_tests ut
|
|
JOIN modules m ON ut.module_id = m.id
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying unit_tests: %w", err)
|
|
}
|
|
for rows.Next() {
|
|
var id int64
|
|
var modName, goMethod string
|
|
if err := rows.Scan(&id, &modName, &goMethod); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
testDBIDs[modName+":"+goMethod] = id
|
|
}
|
|
rows.Close()
|
|
|
|
// Begin transaction
|
|
tx, err := w.db.Begin()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("beginning transaction: %w", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// Clear old call-graph data
|
|
if _, err := tx.Exec("DELETE FROM dependencies WHERE source_type='unit_test' AND dependency_kind='calls'"); err != nil {
|
|
return nil, fmt.Errorf("clearing old dependencies: %w", err)
|
|
}
|
|
if _, err := tx.Exec("UPDATE unit_tests SET feature_id = NULL"); err != nil {
|
|
return nil, fmt.Errorf("clearing old feature_ids: %w", err)
|
|
}
|
|
|
|
// Prepare statements
|
|
insertDep, err := tx.Prepare("INSERT OR IGNORE INTO dependencies (source_type, source_id, target_type, target_id, dependency_kind) VALUES ('unit_test', ?, 'feature', ?, 'calls')")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("preparing insert dependency: %w", err)
|
|
}
|
|
defer insertDep.Close()
|
|
|
|
updateFeatureID, err := tx.Prepare("UPDATE unit_tests SET feature_id = ? WHERE id = ?")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("preparing update feature_id: %w", err)
|
|
}
|
|
defer updateFeatureID.Close()
|
|
|
|
// Process each module's tests
|
|
for _, mod := range result.Modules {
|
|
for _, test := range mod.Tests {
|
|
stats.TestsAnalyzed++
|
|
|
|
testDBID, ok := testDBIDs[mod.Name+":"+test.GoMethod]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Insert dependency rows for linked features
|
|
if len(test.LinkedFeatures) > 0 {
|
|
stats.TestsLinked++
|
|
}
|
|
for _, fi := range test.LinkedFeatures {
|
|
feat := mod.Features[fi]
|
|
featDBID, ok := featureDBIDs[featureKey{mod.Name, feat.GoMethod, feat.GoClass}]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, err := insertDep.Exec(testDBID, featDBID); err != nil {
|
|
return nil, fmt.Errorf("inserting dependency for test %s: %w", test.GoMethod, err)
|
|
}
|
|
stats.DependencyRows++
|
|
}
|
|
|
|
// Set feature_id for best match
|
|
if test.BestFeatureIdx >= 0 {
|
|
feat := mod.Features[test.BestFeatureIdx]
|
|
featDBID, ok := featureDBIDs[featureKey{mod.Name, feat.GoMethod, feat.GoClass}]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, err := updateFeatureID.Exec(featDBID, testDBID); err != nil {
|
|
return nil, fmt.Errorf("updating feature_id for test %s: %w", test.GoMethod, err)
|
|
}
|
|
stats.FeatureIDsSet++
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, fmt.Errorf("committing transaction: %w", err)
|
|
}
|
|
|
|
return stats, nil
|
|
}
|