package db import ( "context" "database/sql" "errors" "fmt" "testing" "time" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" ) func newTestSQLiteDB(t *testing.T) *sqlx.DB { t.Helper() dbConn, err := sqlx.Open("sqlite", ":memory:") if err != nil { t.Fatalf("failed to open sqlite test db: %v", err) } t.Cleanup(func() { _ = dbConn.Close() }) return dbConn } func indexExists(t *testing.T, dbConn *sqlx.DB, name string) bool { t.Helper() var count int if err := dbConn.Get(&count, `SELECT COUNT(1) FROM sqlite_master WHERE type='index' AND name=?`, name); err != nil { t.Fatalf("failed to query index %s: %v", name, err) } return count > 0 } func TestEnsureOncePerDBRetriesUntilSuccess(t *testing.T) { dbConn := newTestSQLiteDB(t) attempts := 0 run := func() error { attempts++ if attempts == 1 { return errors.New("transient failure") } return nil } if err := ensureOncePerDB(dbConn, "test_once", run); err == nil { t.Fatal("expected first ensureOncePerDB call to fail") } if attempts != 1 { t.Fatalf("expected 1 attempt after first call, got %d", attempts) } if err := ensureOncePerDB(dbConn, "test_once", run); err != nil { t.Fatalf("expected second ensureOncePerDB call to succeed, got %v", err) } if attempts != 2 { t.Fatalf("expected 2 attempts after retry, got %d", attempts) } if err := ensureOncePerDB(dbConn, "test_once", run); err != nil { t.Fatalf("expected third ensureOncePerDB call to reuse success, got %v", err) } if attempts != 2 { t.Fatalf("expected no additional attempts after success, got %d", attempts) } } func TestCleanupHourlySnapshotIndexesOlderThan(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) oldTable := "inventory_hourly_1700000000" newTable := "inventory_hourly_1800000000" for _, table := range []string{oldTable, newTable} { if err := EnsureSnapshotTable(ctx, dbConn, table); err != nil { t.Fatalf("failed to create snapshot table %s: %v", table, err) } if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`CREATE INDEX IF NOT EXISTS %s_snapshottime_idx ON %s ("SnapshotTime")`, table, table)); err != nil { t.Fatalf("failed to create snapshottime index for %s: %v", table, err) } if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(`CREATE INDEX IF NOT EXISTS %s_resourcepool_idx ON %s ("ResourcePool")`, table, table)); err != nil { t.Fatalf("failed to create resourcepool index for %s: %v", table, err) } } cutoff := time.Unix(1750000000, 0) dropped, err := CleanupHourlySnapshotIndexesOlderThan(ctx, dbConn, cutoff) if err != nil { t.Fatalf("cleanup failed: %v", err) } if dropped != 3 { t.Fatalf("expected 3 old indexes dropped, got %d", dropped) } oldIndexes := []string{ oldTable + "_vm_vcenter_idx", oldTable + "_snapshottime_idx", oldTable + "_resourcepool_idx", } for _, idx := range oldIndexes { if indexExists(t, dbConn, idx) { t.Fatalf("expected old index %s to be removed", idx) } } newIndexes := []string{ newTable + "_vm_vcenter_idx", newTable + "_snapshottime_idx", newTable + "_resourcepool_idx", } for _, idx := range newIndexes { if !indexExists(t, dbConn, idx) { t.Fatalf("expected recent index %s to remain", idx) } } } func TestFetchVmTraceAndLifecycleUseCacheTables(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if err := EnsureVmHourlyStats(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_hourly_stats: %v", err) } if err := EnsureVmLifecycleCache(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_lifecycle_cache: %v", err) } insertSQL := ` INSERT INTO vm_hourly_stats ( "SnapshotTime","Vcenter","VmId","VmUuid","Name","CreationTime","DeletionTime","ResourcePool", "Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder" ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ` rows := [][]any{ {int64(1000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(900), int64(0), "Tin", "dc", "cluster", "folder", 100.0, int64(2), int64(4), "FALSE", "TRUE", "FALSE"}, {int64(2000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(900), int64(0), "Gold", "dc", "cluster", "folder", 150.0, int64(4), int64(8), "FALSE", "TRUE", "FALSE"}, } for _, args := range rows { if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil { t.Fatalf("failed to insert hourly cache row: %v", err) } } if err := UpsertVmLifecycleCache(ctx, dbConn, "vc-a", "vm-1", "uuid-1", "demo-vm", "cluster", time.Unix(1000, 0), sql.NullInt64{Int64: 900, Valid: true}); err != nil { t.Fatalf("failed to upsert lifecycle cache: %v", err) } if err := MarkVmDeletedWithDetails(ctx, dbConn, "vc-a", "vm-1", "uuid-1", "demo-vm", "cluster", 2500); err != nil { t.Fatalf("failed to mark vm deleted: %v", err) } traceRows, err := FetchVmTrace(ctx, dbConn, "vm-1", "", "") if err != nil { t.Fatalf("FetchVmTrace failed: %v", err) } if len(traceRows) != 2 { t.Fatalf("expected 2 trace rows, got %d", len(traceRows)) } if traceRows[0].SnapshotTime != 1000 || traceRows[1].SnapshotTime != 2000 { t.Fatalf("trace rows are not sorted by snapshot time: %#v", traceRows) } traceRowsByName, err := FetchVmTrace(ctx, dbConn, "", "", "DEMO-VM") if err != nil { t.Fatalf("FetchVmTrace by name failed: %v", err) } if len(traceRowsByName) != 2 { t.Fatalf("expected 2 trace rows by name, got %d", len(traceRowsByName)) } emptyTraceRows, err := FetchVmTrace(ctx, dbConn, "", "", "") if err != nil { t.Fatalf("FetchVmTrace with empty identifier failed: %v", err) } if len(emptyTraceRows) != 0 { t.Fatalf("expected 0 trace rows for empty identifier, got %d", len(emptyTraceRows)) } lifecycle, err := FetchVmLifecycle(ctx, dbConn, "vm-1", "", "") if err != nil { t.Fatalf("FetchVmLifecycle failed: %v", err) } if lifecycle.FirstSeen != 900 { t.Fatalf("expected FirstSeen=900 (earliest known from lifecycle cache), got %d", lifecycle.FirstSeen) } if lifecycle.LastSeen != 2000 { t.Fatalf("expected LastSeen=2000, got %d", lifecycle.LastSeen) } if lifecycle.CreationTime != 900 || lifecycle.CreationApprox { t.Fatalf("expected exact CreationTime=900, got time=%d approx=%v", lifecycle.CreationTime, lifecycle.CreationApprox) } if lifecycle.DeletionTime != 2500 { t.Fatalf("expected DeletionTime=2500 from lifecycle cache, got %d", lifecycle.DeletionTime) } lifecycleByName, err := FetchVmLifecycle(ctx, dbConn, "", "", "DEMO-VM") if err != nil { t.Fatalf("FetchVmLifecycle by name failed: %v", err) } if lifecycleByName.FirstSeen != 900 || lifecycleByName.LastSeen != 2000 { t.Fatalf("unexpected lifecycle for name lookup: %#v", lifecycleByName) } emptyLifecycle, err := FetchVmLifecycle(ctx, dbConn, "", "", "") if err != nil { t.Fatalf("FetchVmLifecycle with empty identifier failed: %v", err) } if emptyLifecycle.FirstSeen != 0 || emptyLifecycle.LastSeen != 0 || emptyLifecycle.CreationTime != 0 || emptyLifecycle.DeletionTime != 0 { t.Fatalf("expected empty lifecycle for empty identifier, got %#v", emptyLifecycle) } } func TestFetchVmTraceDailyFromRollup(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if err := EnsureVmDailyRollup(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_daily_rollup: %v", err) } if err := UpsertVmDailyRollup(ctx, dbConn, 1700000000, VmDailyRollupRow{ Vcenter: "vc-a", VmId: "vm-1", VmUuid: "uuid-1", Name: "demo-vm", CreationTime: 1699999000, SamplesPresent: 8, SumVcpu: 32, SumRam: 64, LastVcpuCount: 4, LastRamGB: 8, LastResourcePool: "Tin", }); err != nil { t.Fatalf("failed to insert daily rollup row 1: %v", err) } if err := UpsertVmDailyRollup(ctx, dbConn, 1700086400, VmDailyRollupRow{ Vcenter: "vc-a", VmId: "vm-1", VmUuid: "uuid-1", Name: "demo-vm", CreationTime: 1699999000, SamplesPresent: 4, SumVcpu: 20, SumRam: 36, LastVcpuCount: 5, LastRamGB: 9, LastResourcePool: "Gold", LastProvisionedDisk: 150.5, }); err != nil { t.Fatalf("failed to insert daily rollup row 2: %v", err) } rows, err := FetchVmTraceDaily(ctx, dbConn, "vm-1", "", "") if err != nil { t.Fatalf("FetchVmTraceDaily failed: %v", err) } if len(rows) != 2 { t.Fatalf("expected 2 daily trace rows, got %d", len(rows)) } if rows[0].SnapshotTime != 1700000000 || rows[0].VcpuCount != 4 || rows[0].RamGB != 8 { t.Fatalf("unexpected first daily row: %#v", rows[0]) } if rows[1].SnapshotTime != 1700086400 || rows[1].VcpuCount != 5 || rows[1].RamGB != 9 || rows[1].ProvisionedDisk != 150.5 { t.Fatalf("unexpected second daily row: %#v", rows[1]) } } func TestFetchVmTraceDailyFallbackToSummaryTables(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if _, err := dbConn.ExecContext(ctx, ` CREATE TABLE snapshot_registry ( snapshot_type TEXT, table_name TEXT, snapshot_time BIGINT )`); err != nil { t.Fatalf("failed to create snapshot_registry: %v", err) } summaryTable := "inventory_daily_summary_20260106" if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` CREATE TABLE %s ( "Name" TEXT, "Vcenter" TEXT, "VmId" TEXT, "VmUuid" TEXT, "ResourcePool" TEXT, "AvgVcpuCount" REAL, "AvgRamGB" REAL, "AvgProvisionedDisk" REAL, "CreationTime" BIGINT, "DeletionTime" BIGINT )`, summaryTable)); err != nil { t.Fatalf("failed to create summary table: %v", err) } if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` INSERT INTO %s ("Name","Vcenter","VmId","VmUuid","ResourcePool","AvgVcpuCount","AvgRamGB","AvgProvisionedDisk","CreationTime","DeletionTime") VALUES (?,?,?,?,?,?,?,?,?,?) `, summaryTable), "demo-vm", "vc-a", "vm-1", "uuid-1", "Silver", 3.2, 6.7, 123.4, int64(1699999000), int64(0)); err != nil { t.Fatalf("failed to insert summary row: %v", err) } if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", summaryTable, int64(1700500000)); err != nil { t.Fatalf("failed to insert snapshot_registry row: %v", err) } rows, err := FetchVmTraceDaily(ctx, dbConn, "", "uuid-1", "") if err != nil { t.Fatalf("FetchVmTraceDaily fallback failed: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 fallback daily row, got %d", len(rows)) } if rows[0].SnapshotTime != 1700500000 || rows[0].VcpuCount != 3 || rows[0].RamGB != 6 { t.Fatalf("unexpected fallback daily row: %#v", rows[0]) } } func TestClearVcenterReferenceCache(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if err := EnsureVcenterReferenceCacheTables(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vcenter reference cache tables: %v", err) } if err := UpsertVcenterFolderCache(ctx, dbConn, "vc-a", "group-v123", "/Datacenters/DC1/vm/Prod", 1000); err != nil { t.Fatalf("failed to upsert folder cache: %v", err) } if err := UpsertVcenterResourcePoolCache(ctx, dbConn, "vc-a", "resgroup-1", "Gold", 1000); err != nil { t.Fatalf("failed to upsert resource pool cache: %v", err) } if err := UpsertVcenterHostCache(ctx, dbConn, "vc-a", "host-123", "Cluster-1", "DC1", 1000); err != nil { t.Fatalf("failed to upsert host cache: %v", err) } if err := ClearVcenterReferenceCache(ctx, dbConn, "vc-a"); err != nil { t.Fatalf("failed to clear vcenter reference cache: %v", err) } var folderCount int if err := dbConn.Get(&folderCount, `SELECT COUNT(1) FROM vcenter_folder_cache WHERE "Vcenter" = ?`, "vc-a"); err != nil { t.Fatalf("failed to count folder cache rows: %v", err) } if folderCount != 0 { t.Fatalf("expected 0 folder cache rows after clear, got %d", folderCount) } var poolCount int if err := dbConn.Get(&poolCount, `SELECT COUNT(1) FROM vcenter_resource_pool_cache WHERE "Vcenter" = ?`, "vc-a"); err != nil { t.Fatalf("failed to count resource pool cache rows: %v", err) } if poolCount != 0 { t.Fatalf("expected 0 resource pool cache rows after clear, got %d", poolCount) } var hostCount int if err := dbConn.Get(&hostCount, `SELECT COUNT(1) FROM vcenter_host_cache WHERE "Vcenter" = ?`, "vc-a"); err != nil { t.Fatalf("failed to count host cache rows: %v", err) } if hostCount != 0 { t.Fatalf("expected 0 host cache rows after clear, got %d", hostCount) } } func TestFetchVmLifecycleIgnoresStaleDeletionFromHourlyCache(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if err := EnsureVmHourlyStats(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_hourly_stats: %v", err) } insertSQL := ` INSERT INTO vm_hourly_stats ( "SnapshotTime","Vcenter","VmId","VmUuid","Name","CreationTime","DeletionTime","ResourcePool", "Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder" ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ` // First row carries an old deletion marker, later row proves VM is still present. rows := [][]any{ {int64(1700000000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(1699999000), int64(1700003600), "Tin", "dc", "cluster", "folder", 100.0, int64(2), int64(4), "FALSE", "TRUE", "FALSE"}, {int64(1700100000), "vc-a", "vm-1", "uuid-1", "demo-vm", int64(1699999000), int64(0), "Gold", "dc", "cluster", "folder", 120.0, int64(4), int64(8), "FALSE", "TRUE", "FALSE"}, } for _, args := range rows { if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil { t.Fatalf("failed to insert hourly cache row: %v", err) } } lifecycle, err := FetchVmLifecycle(ctx, dbConn, "vm-1", "", "") if err != nil { t.Fatalf("FetchVmLifecycle failed: %v", err) } if lifecycle.LastSeen != 1700100000 { t.Fatalf("expected LastSeen=1700100000, got %d", lifecycle.LastSeen) } if lifecycle.DeletionTime != 0 { t.Fatalf("expected stale deletion to be ignored, got %d", lifecycle.DeletionTime) } lifecycleDiag, diag, err := FetchVmLifecycleWithDiagnostics(ctx, dbConn, "vm-1", "", "") if err != nil { t.Fatalf("FetchVmLifecycleWithDiagnostics failed: %v", err) } if lifecycleDiag.DeletionTime != 0 { t.Fatalf("expected stale deletion to be ignored in diagnostics path, got %d", lifecycleDiag.DeletionTime) } if !diag.HourlyCache.StaleDeletionIgnored { t.Fatalf("expected hourly cache diagnostics to flag stale deletion ignore, got %#v", diag.HourlyCache) } if diag.HourlyCache.DeletionMax != 1700003600 { t.Fatalf("expected hourly cache deletion max 1700003600, got %d", diag.HourlyCache.DeletionMax) } if diag.FinalLifecycle.LastSeen != 1700100000 || diag.FinalLifecycle.DeletionTime != 0 { t.Fatalf("unexpected final diagnostics lifecycle: %#v", diag.FinalLifecycle) } } func TestParseHourlySnapshotUnix(t *testing.T) { cases := []struct { table string ok bool val int64 }{ {table: "inventory_hourly_1700000000", ok: true, val: 1700000000}, {table: "inventory_hourly_bad", ok: false, val: 0}, {table: "inventory_daily_summary_20260101", ok: false, val: 0}, } for _, tc := range cases { got, ok := parseHourlySnapshotUnix(tc.table) if ok != tc.ok || got != tc.val { t.Fatalf("parseHourlySnapshotUnix(%q) = (%d,%v), expected (%d,%v)", tc.table, got, ok, tc.val, tc.ok) } } } func TestVcenterLatestTotalsAndListFallback(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if err := EnsureVcenterLatestTotalsTable(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vcenter_latest_totals: %v", err) } if err := InsertVcenterTotals(ctx, dbConn, "vc-a", time.Unix(200, 0), 10, 20, 30); err != nil { t.Fatalf("failed to insert totals for vc-a: %v", err) } // Older snapshot should not replace latest totals. if err := InsertVcenterTotals(ctx, dbConn, "vc-a", time.Unix(100, 0), 1, 2, 3); err != nil { t.Fatalf("failed to insert older totals for vc-a: %v", err) } if err := InsertVcenterTotals(ctx, dbConn, "vc-b", time.Unix(300, 0), 11, 21, 31); err != nil { t.Fatalf("failed to insert totals for vc-b: %v", err) } vcenters, err := ListVcenters(ctx, dbConn) if err != nil { t.Fatalf("ListVcenters failed: %v", err) } if len(vcenters) != 2 || vcenters[0] != "vc-a" || vcenters[1] != "vc-b" { t.Fatalf("unexpected vcenter list: %#v", vcenters) } var latest struct { SnapshotTime int64 `db:"SnapshotTime"` VmCount int64 `db:"VmCount"` VcpuTotal int64 `db:"VcpuTotal"` RamTotalGB int64 `db:"RamTotalGB"` } if err := dbConn.GetContext(ctx, &latest, ` SELECT "SnapshotTime","VmCount","VcpuTotal","RamTotalGB" FROM vcenter_latest_totals WHERE "Vcenter" = ? `, "vc-a"); err != nil { t.Fatalf("failed to query latest totals for vc-a: %v", err) } if latest.SnapshotTime != 200 || latest.VmCount != 10 || latest.VcpuTotal != 20 || latest.RamTotalGB != 30 { t.Fatalf("unexpected latest totals for vc-a: %#v", latest) } } func TestListVcenterHourlyTotalsSince(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) base := time.Unix(1_700_000_000, 0) if err := InsertVcenterTotals(ctx, dbConn, "vc-a", base.AddDate(0, 0, -60), 1, 2, 3); err != nil { t.Fatalf("failed to insert old totals: %v", err) } if err := InsertVcenterTotals(ctx, dbConn, "vc-a", base.AddDate(0, 0, -10), 10, 20, 30); err != nil { t.Fatalf("failed to insert recent totals: %v", err) } if err := InsertVcenterTotals(ctx, dbConn, "vc-b", base.AddDate(0, 0, -5), 100, 200, 300); err != nil { t.Fatalf("failed to insert other-vcenter totals: %v", err) } rows, err := ListVcenterHourlyTotalsSince(ctx, dbConn, "vc-a", base.AddDate(0, 0, -45)) if err != nil { t.Fatalf("ListVcenterHourlyTotalsSince failed: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 row for vc-a since cutoff, got %d", len(rows)) } if rows[0].SnapshotTime != base.AddDate(0, 0, -10).Unix() || rows[0].VmCount != 10 { t.Fatalf("unexpected row returned: %#v", rows[0]) } } func TestInsertVcenterTotalsUpsertsHourlyAggregate(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) snapshotTime := time.Unix(1_700_000_500, 0) if err := InsertVcenterTotals(ctx, dbConn, "vc-a", snapshotTime, 12, 24, 48); err != nil { t.Fatalf("InsertVcenterTotals failed: %v", err) } rows, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "hourly", 10) if err != nil { t.Fatalf("ListVcenterAggregateTotals failed: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 hourly aggregate row, got %d", len(rows)) } if rows[0].SnapshotTime != snapshotTime.Unix() || rows[0].VmCount != 12 || rows[0].VcpuTotal != 24 || rows[0].RamTotalGB != 48 { t.Fatalf("unexpected hourly aggregate row: %#v", rows[0]) } } func TestListVcenterHourlyTotalsSinceUsesAggregateCache(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) base := time.Unix(1_700_000_000, 0) if err := UpsertVcenterAggregateTotal(ctx, dbConn, "hourly", "vc-a", base.Unix(), 7, 14, 21); err != nil { t.Fatalf("UpsertVcenterAggregateTotal failed: %v", err) } rows, err := ListVcenterHourlyTotalsSince(ctx, dbConn, "vc-a", base.Add(-24*time.Hour)) if err != nil { t.Fatalf("ListVcenterHourlyTotalsSince failed: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 cached row, got %d", len(rows)) } if rows[0].SnapshotTime != base.Unix() || rows[0].VmCount != 7 || rows[0].VcpuTotal != 14 || rows[0].RamTotalGB != 21 { t.Fatalf("unexpected cached hourly row: %#v", rows[0]) } } func TestReplaceVcenterAggregateTotalsFromSummary(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) summaryTable := "inventory_daily_summary_20260101" if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` CREATE TABLE %s ( "Vcenter" TEXT NOT NULL, "Name" TEXT, "VmId" TEXT, "VmUuid" TEXT, "AvgVcpuCount" REAL, "VcpuCount" BIGINT, "AvgRamGB" REAL, "RamGB" BIGINT )`, summaryTable)); err != nil { t.Fatalf("failed to create summary table: %v", err) } insertSQL := fmt.Sprintf(` INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?) `, summaryTable) rows := [][]any{ {"vc-a", "vm-1", "1", "u1", 2.0, 4.0}, {"vc-a", "vm-2", "2", "u2", 3.0, 5.0}, {"vc-b", "vm-3", "3", "u3", 1.0, 2.0}, } for _, args := range rows { if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil { t.Fatalf("failed to insert summary row: %v", err) } } upserted, err := ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", 1_700_010_000) if err != nil { t.Fatalf("ReplaceVcenterAggregateTotalsFromSummary failed: %v", err) } if upserted != 2 { t.Fatalf("expected 2 vcenter aggregate rows, got %d", upserted) } vcA, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10) if err != nil { t.Fatalf("ListVcenterAggregateTotals(vc-a) failed: %v", err) } if len(vcA) != 1 { t.Fatalf("expected 1 vc-a daily row, got %d", len(vcA)) } if vcA[0].SnapshotTime != 1_700_010_000 || vcA[0].VmCount != 2 || vcA[0].VcpuTotal != 5 || vcA[0].RamTotalGB != 9 { t.Fatalf("unexpected vc-a daily aggregate row: %#v", vcA[0]) } vcB, err := ListVcenterAggregateTotalsSince(ctx, dbConn, "vc-b", "daily", time.Unix(1_700_009_000, 0)) if err != nil { t.Fatalf("ListVcenterAggregateTotalsSince(vc-b) failed: %v", err) } if len(vcB) != 1 || vcB[0].VmCount != 1 || vcB[0].VcpuTotal != 1 || vcB[0].RamTotalGB != 2 { t.Fatalf("unexpected vc-b daily aggregate row: %#v", vcB) } } func TestListVcenterTotalsByTypeDailyFallbackWarmsCache(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if _, err := dbConn.ExecContext(ctx, ` CREATE TABLE snapshot_registry ( snapshot_type TEXT, table_name TEXT, snapshot_time BIGINT )`); err != nil { t.Fatalf("failed to create snapshot_registry: %v", err) } summaryTable := "inventory_daily_summary_20260102" if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` CREATE TABLE %s ( "Vcenter" TEXT NOT NULL, "Name" TEXT, "VmId" TEXT, "VmUuid" TEXT, "AvgVcpuCount" REAL, "VcpuCount" BIGINT, "AvgRamGB" REAL, "RamGB" BIGINT )`, summaryTable)); err != nil { t.Fatalf("failed to create summary table: %v", err) } insertSQL := fmt.Sprintf(` INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?) `, summaryTable) for _, args := range [][]any{ {"vc-a", "vm-1", "1", "u1", 4.0, 8.0}, {"vc-a", "vm-2", "2", "u2", 2.0, 6.0}, } { if _, err := dbConn.ExecContext(ctx, insertSQL, args...); err != nil { t.Fatalf("failed to insert summary row: %v", err) } } if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", summaryTable, int64(1_700_020_000)); err != nil { t.Fatalf("failed to insert snapshot_registry row: %v", err) } rows, err := ListVcenterTotalsByType(ctx, dbConn, "vc-a", "daily", 10) if err != nil { t.Fatalf("ListVcenterTotalsByType failed: %v", err) } if len(rows) != 1 { t.Fatalf("expected 1 daily row, got %d", len(rows)) } if rows[0].SnapshotTime != 1_700_020_000 || rows[0].VmCount != 2 || rows[0].VcpuTotal != 6 || rows[0].RamTotalGB != 14 { t.Fatalf("unexpected daily totals row: %#v", rows[0]) } cached, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10) if err != nil { t.Fatalf("ListVcenterAggregateTotals failed: %v", err) } if len(cached) != 1 || cached[0].SnapshotTime != 1_700_020_000 || cached[0].VmCount != 2 { t.Fatalf("expected warmed daily cache row, got %#v", cached) } } func TestSyncVcenterAggregateTotalsFromRegistry(t *testing.T) { ctx := context.Background() dbConn := newTestSQLiteDB(t) if _, err := dbConn.ExecContext(ctx, ` CREATE TABLE snapshot_registry ( snapshot_type TEXT, table_name TEXT, snapshot_time BIGINT )`); err != nil { t.Fatalf("failed to create snapshot_registry: %v", err) } table1 := "inventory_daily_summary_20260103" table2 := "inventory_daily_summary_20260104" for _, table := range []string{table1, table2} { if _, err := dbConn.ExecContext(ctx, fmt.Sprintf(` CREATE TABLE %s ( "Vcenter" TEXT NOT NULL, "Name" TEXT, "VmId" TEXT, "VmUuid" TEXT, "AvgVcpuCount" REAL, "VcpuCount" BIGINT, "AvgRamGB" REAL, "RamGB" BIGINT )`, table)); err != nil { t.Fatalf("failed to create summary table %s: %v", table, err) } } insert1 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table1) insert2 := fmt.Sprintf(`INSERT INTO %s ("Vcenter","Name","VmId","VmUuid","AvgVcpuCount","AvgRamGB") VALUES (?,?,?,?,?,?)`, table2) for _, args := range [][]any{ {"vc-a", "vm-1", "1", "u1", 2.0, 4.0}, {"vc-b", "vm-2", "2", "u2", 3.0, 5.0}, } { if _, err := dbConn.ExecContext(ctx, insert1, args...); err != nil { t.Fatalf("failed to insert row into %s: %v", table1, err) } } if _, err := dbConn.ExecContext(ctx, insert2, "vc-a", "vm-3", "3", "u3", 4.0, 6.0); err != nil { t.Fatalf("failed to insert row into %s: %v", table2, err) } if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", table1, int64(1_700_030_000)); err != nil { t.Fatalf("failed to insert snapshot_registry row for table1: %v", err) } if _, err := dbConn.ExecContext(ctx, `INSERT INTO snapshot_registry (snapshot_type, table_name, snapshot_time) VALUES (?,?,?)`, "daily", table2, int64(1_700_040_000)); err != nil { t.Fatalf("failed to insert snapshot_registry row for table2: %v", err) } snapshotsRefreshed, rowsUpserted, err := SyncVcenterAggregateTotalsFromRegistry(ctx, dbConn, "daily") if err != nil { t.Fatalf("SyncVcenterAggregateTotalsFromRegistry failed: %v", err) } if snapshotsRefreshed != 2 { t.Fatalf("expected 2 snapshots refreshed, got %d", snapshotsRefreshed) } if rowsUpserted != 3 { t.Fatalf("expected 3 rows upserted, got %d", rowsUpserted) } rows, err := ListVcenterAggregateTotals(ctx, dbConn, "vc-a", "daily", 10) if err != nil { t.Fatalf("ListVcenterAggregateTotals failed: %v", err) } if len(rows) != 2 { t.Fatalf("expected 2 daily rows for vc-a, got %d", len(rows)) } if rows[0].SnapshotTime != 1_700_040_000 || rows[0].VmCount != 1 || rows[0].VcpuTotal != 4 || rows[0].RamTotalGB != 6 { t.Fatalf("unexpected latest vc-a daily row: %#v", rows[0]) } if rows[1].SnapshotTime != 1_700_030_000 || rows[1].VmCount != 1 || rows[1].VcpuTotal != 2 || rows[1].RamTotalGB != 4 { t.Fatalf("unexpected older vc-a daily row: %#v", rows[1]) } }