diff --git a/db/helpers.go b/db/helpers.go index 82a243a..6c7f894 100644 --- a/db/helpers.go +++ b/db/helpers.go @@ -498,12 +498,73 @@ GROUP BY agg."InventoryId", agg."Name", agg."Vcenter", agg."VmId", agg."EventKey", agg."CloudId", agg."Datacenter", agg."Cluster", agg."Folder", agg."ProvisionedDisk", agg."VcpuCount", agg."RamGB", agg."IsTemplate", agg."PoweredOn", agg."SrmPlaceholder", agg."VmUuid", - agg.any_creation, agg.any_deletion, agg.first_present, agg.last_present, + agg.any_creation, agg.any_deletion, agg.first_present, agg.last_present, totals.total_samples; `, unionQuery, tableName) return insert, nil } +// RefineCreationDeletionFromUnion walks all snapshot rows in a period and tightens CreationTime/DeletionTime +// by using the first and last observed samples and the first sample after disappearance. +func RefineCreationDeletionFromUnion(ctx context.Context, dbConn *sqlx.DB, summaryTable, unionQuery string) error { + if unionQuery == "" { + return fmt.Errorf("union query is empty") + } + if _, err := SafeTableName(summaryTable); err != nil { + return err + } + sql := fmt.Sprintf(` +WITH snapshots AS ( +%s +), timeline AS ( + SELECT + s."VmId", + s."VmUuid", + s."Name", + s."Vcenter", + MIN(NULLIF(s."CreationTime", 0)) AS any_creation, + MIN(s."SnapshotTime") AS first_seen, + MAX(s."SnapshotTime") AS last_seen + FROM snapshots s + GROUP BY s."VmId", s."VmUuid", s."Name", s."Vcenter" +) +UPDATE %s dst +SET + "CreationTime" = CASE + WHEN t.any_creation IS NOT NULL AND t.any_creation > 0 THEN LEAST(COALESCE(NULLIF(dst."CreationTime", 0), t.any_creation), t.any_creation) + WHEN t.first_seen IS NOT NULL THEN LEAST(COALESCE(NULLIF(dst."CreationTime", 0), t.first_seen), t.first_seen) + ELSE dst."CreationTime" + END, + "DeletionTime" = CASE + WHEN t_last_after IS NOT NULL + AND (dst."DeletionTime" IS NULL OR dst."DeletionTime" = 0 OR t_last_after < dst."DeletionTime") + THEN t_last_after + ELSE dst."DeletionTime" + END +FROM ( + SELECT + tl.*, + ( + SELECT MIN(s2."SnapshotTime") + FROM snapshots s2 + WHERE s2."Vcenter" = tl."Vcenter" + AND COALESCE(s2."VmId", '') = COALESCE(tl."VmId", '') + AND s2."SnapshotTime" > tl.last_seen + ) AS t_last_after + FROM timeline tl +) t +WHERE dst."Vcenter" = t."Vcenter" + AND ( + (dst."VmId" IS NOT DISTINCT FROM t."VmId") + OR (dst."VmUuid" IS NOT DISTINCT FROM t."VmUuid") + OR (dst."Name" IS NOT DISTINCT FROM t."Name") + ); +`, unionQuery, summaryTable) + + _, err := dbConn.ExecContext(ctx, sql) + return err +} + // BuildMonthlySummaryInsert returns the SQL to aggregate daily summaries into a monthly summary table. func BuildMonthlySummaryInsert(tableName string, unionQuery string) (string, error) { if _, err := SafeTableName(tableName); err != nil { diff --git a/internal/tasks/dailyAggregate.go b/internal/tasks/dailyAggregate.go index 0917945..b4b91cc 100644 --- a/internal/tasks/dailyAggregate.go +++ b/internal/tasks/dailyAggregate.go @@ -129,6 +129,9 @@ func (c *CronTask) aggregateDailySummary(ctx context.Context, targetTime time.Ti c.Logger.Error("failed to aggregate daily inventory", "error", err, "date", dayStart.Format("2006-01-02")) return err } + if err := db.RefineCreationDeletionFromUnion(ctx, dbConn, summaryTable, unionQuery); err != nil { + c.Logger.Warn("failed to refine creation/deletion times", "error", err, "table", summaryTable) + } rowCount, err := db.TableRowCount(ctx, dbConn, summaryTable) if err != nil { c.Logger.Warn("unable to count daily summary rows", "error", err, "table", summaryTable)