a802f59f55
- cmd/fn/doctor.go - cmd/fn/main.go - cpp/apps/primitives_gallery/playground/tables/CMakeLists.txt - cpp/apps/primitives_gallery/playground/tables/data_table.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.cpp - cpp/apps/primitives_gallery/playground/tables/data_table_logic.h - cpp/apps/primitives_gallery/playground/tables/self_test.cpp - cpp/apps/primitives_gallery/playground/tables/tql.cpp - cpp/apps/primitives_gallery/playground/tables/viz.cpp - cpp/apps/primitives_gallery/playground/tables/viz.h - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
package infra
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// AggregateReport summarises the result of a VaultAggregateIndex run.
|
|
type AggregateReport struct {
|
|
VaultsProcessed int
|
|
VaultsSkipped int // vaults without a vault_index.db
|
|
TotalFiles int
|
|
Errors []string // non-fatal per-vault errors
|
|
}
|
|
|
|
// VaultAggregateIndex reads all vault manifests from repoRoot, opens each
|
|
// vault_index.db and copies all file records into the central registry.db
|
|
// vault_files table. The table is created if it does not exist (idempotent).
|
|
//
|
|
// For each vault the previous rows are deleted and replaced atomically, so
|
|
// re-running always produces a clean, non-duplicated state.
|
|
//
|
|
// Returns an AggregateReport with counts. Per-vault errors are non-fatal
|
|
// (logged in report.Errors); only fatal errors (e.g. registry.db
|
|
// unreachable) are returned as the error value.
|
|
func VaultAggregateIndex(repoRoot string) (AggregateReport, error) {
|
|
var report AggregateReport
|
|
|
|
// 1. Open registry.db
|
|
registryDB, err := SQLiteOpen(filepath.Join(repoRoot, "registry.db"), "")
|
|
if err != nil {
|
|
return report, fmt.Errorf("vault_aggregate_index: open registry.db: %w", err)
|
|
}
|
|
defer registryDB.Close()
|
|
|
|
// 2. Idempotent schema migration
|
|
for _, stmt := range []string{
|
|
`CREATE TABLE IF NOT EXISTS vault_files (
|
|
vault_id TEXT NOT NULL,
|
|
vault_name TEXT NOT NULL,
|
|
rel_path TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
mtime INTEGER NOT NULL,
|
|
sha256 TEXT NOT NULL,
|
|
mime TEXT NOT NULL DEFAULT '',
|
|
ext TEXT NOT NULL DEFAULT '',
|
|
bucket TEXT NOT NULL DEFAULT '',
|
|
sub_bucket TEXT NOT NULL DEFAULT '',
|
|
indexed_at INTEGER NOT NULL,
|
|
PRIMARY KEY (vault_id, rel_path)
|
|
);`,
|
|
`CREATE INDEX IF NOT EXISTS idx_vault_files_sha256 ON vault_files(sha256);`,
|
|
`CREATE INDEX IF NOT EXISTS idx_vault_files_vault ON vault_files(vault_id);`,
|
|
} {
|
|
if _, err := registryDB.Exec(stmt); err != nil {
|
|
if !isIdempotentMigrationError(err) {
|
|
return report, fmt.Errorf("vault_aggregate_index: schema: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Read manifest
|
|
entries, err := VaultManifestRead(repoRoot)
|
|
if err != nil {
|
|
return report, fmt.Errorf("vault_aggregate_index: manifest: %w", err)
|
|
}
|
|
|
|
now := time.Now().UTC().Unix()
|
|
|
|
for _, entry := range entries {
|
|
vaultID := vaultIDFromEntry(entry)
|
|
vaultName := entry.Name
|
|
vaultPath := entry.Path
|
|
|
|
indexPath := filepath.Join(vaultPath, "vault_index.db")
|
|
if _, statErr := os.Stat(indexPath); statErr != nil {
|
|
report.VaultsSkipped++
|
|
continue
|
|
}
|
|
|
|
vaultDB, openErr := VaultIndexOpen(vaultPath)
|
|
if openErr != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: open index: %v", vaultName, openErr))
|
|
continue
|
|
}
|
|
|
|
rows, queryErr := vaultDB.Query(
|
|
`SELECT rel_path, size, mtime, sha256, mime, ext, bucket, sub_bucket FROM files`,
|
|
)
|
|
if queryErr != nil {
|
|
vaultDB.Close()
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: query files: %v", vaultName, queryErr))
|
|
continue
|
|
}
|
|
|
|
type fileRow struct {
|
|
RelPath string
|
|
Size int64
|
|
Mtime int64
|
|
Sha256 string
|
|
Mime string
|
|
Ext string
|
|
Bucket string
|
|
SubBucket string
|
|
}
|
|
var fileRows []fileRow
|
|
for rows.Next() {
|
|
var r fileRow
|
|
if scanErr := rows.Scan(&r.RelPath, &r.Size, &r.Mtime, &r.Sha256, &r.Mime, &r.Ext, &r.Bucket, &r.SubBucket); scanErr != nil {
|
|
continue
|
|
}
|
|
fileRows = append(fileRows, r)
|
|
}
|
|
rows.Close()
|
|
vaultDB.Close()
|
|
|
|
// Atomic replace in registry.db
|
|
tx, txErr := registryDB.Begin()
|
|
if txErr != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: begin tx: %v", vaultName, txErr))
|
|
continue
|
|
}
|
|
|
|
if _, delErr := tx.Exec(`DELETE FROM vault_files WHERE vault_id = ?`, vaultID); delErr != nil {
|
|
tx.Rollback()
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: delete: %v", vaultName, delErr))
|
|
continue
|
|
}
|
|
|
|
stmt, prepErr := tx.Prepare(`
|
|
INSERT INTO vault_files
|
|
(vault_id, vault_name, rel_path, size, mtime, sha256, mime, ext, bucket, sub_bucket, indexed_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
if prepErr != nil {
|
|
tx.Rollback()
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: prepare: %v", vaultName, prepErr))
|
|
continue
|
|
}
|
|
|
|
for _, r := range fileRows {
|
|
if _, insErr := stmt.Exec(vaultID, vaultName, r.RelPath, r.Size, r.Mtime, r.Sha256, r.Mime, r.Ext, r.Bucket, r.SubBucket, now); insErr != nil {
|
|
stmt.Close()
|
|
tx.Rollback()
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: insert %s: %v", vaultName, r.RelPath, insErr))
|
|
continue
|
|
}
|
|
}
|
|
stmt.Close()
|
|
|
|
if commitErr := tx.Commit(); commitErr != nil {
|
|
report.Errors = append(report.Errors, fmt.Sprintf("%s: commit: %v", vaultName, commitErr))
|
|
continue
|
|
}
|
|
|
|
report.VaultsProcessed++
|
|
report.TotalFiles += len(fileRows)
|
|
}
|
|
|
|
return report, nil
|
|
}
|
|
|
|
// vaultIDFromEntry constructs the canonical vault ID used in registry.db.
|
|
// Pattern: "<vault_name>_<project_id>" — consistent with the vaults table.
|
|
func vaultIDFromEntry(e VaultManifestEntry) string {
|
|
if e.ProjectID == "" {
|
|
return e.Name
|
|
}
|
|
return e.Name + "_" + e.ProjectID
|
|
}
|