diff --git a/internal/report/snapshots.go b/internal/report/snapshots.go index 6e22d37..d3808d8 100644 --- a/internal/report/snapshots.go +++ b/internal/report/snapshots.go @@ -1145,10 +1145,35 @@ func buildHourlyTotals(ctx context.Context, logger *slog.Logger, dbConn *sqlx.DB recordIndex = selectedIndex continue } + var prev *SnapshotRecord + for prevIndex := selectedIndex - 1; prevIndex >= 0; prevIndex-- { + record := records[prevIndex] + if record.SnapshotTime.After(hourEnd) { + continue + } + if err := db.ValidateTableName(record.TableName); err != nil { + return nil, err + } + if record.SnapshotCount == 0 { + continue + } + if record.SnapshotCount < 0 { + rowsExist, err := db.TableHasRows(ctx, dbConn, record.TableName) + if err != nil || !rowsExist { + continue + } + } + prev = &record + break + } recordIndex = selectedIndex hourStartUnix := hourWindowStart.Unix() hourEndUnix := hourEnd.Unix() durationSeconds := float64(hourEndUnix - hourStartUnix) + prevTableName := selected.TableName + if prev != nil { + prevTableName = prev.TableName + } startExpr := `CASE WHEN "CreationTime" IS NOT NULL AND "CreationTime" > 0 AND "CreationTime" > ? THEN "CreationTime" ELSE ? END` endExpr := `CASE WHEN "DeletionTime" IS NOT NULL AND "DeletionTime" > 0 AND "DeletionTime" < ? THEN "DeletionTime" ELSE ? END` overlapExpr := fmt.Sprintf(`CASE WHEN %s > %s THEN (CAST((%s - %s) AS REAL) / ?) ELSE 0 END`, endExpr, startExpr, endExpr, startExpr) @@ -1193,6 +1218,44 @@ lifecycle AS ( FROM vm_lifecycle_cache GROUP BY vm_key ), +prev_base AS ( + SELECT + %s AS vm_key, + "VcpuCount", + "RamGB", + LOWER(COALESCE("ResourcePool", '')) AS pool, + NULLIF("CreationTime", 0) AS creation_time, + NULLIF("DeletionTime", 0) AS deletion_time + FROM %s + WHERE %s +), +prev_agg AS ( + SELECT + vm_key, + MAX("VcpuCount") AS "VcpuCount", + MAX("RamGB") AS "RamGB", + MAX(pool) AS pool, + MIN(creation_time) AS creation_time, + MIN(deletion_time) AS deletion_time + FROM prev_base + GROUP BY vm_key +), +missing_deleted AS ( + SELECT + prev_agg.vm_key, + prev_agg."VcpuCount", + prev_agg."RamGB", + prev_agg.pool, + prev_agg.creation_time AS creation_time, + COALESCE(lifecycle.deleted_at, prev_agg.deletion_time) AS deletion_time, + %s AS presence + FROM prev_agg + LEFT JOIN lifecycle ON lifecycle.vm_key = prev_agg.vm_key + LEFT JOIN agg ON agg.vm_key = prev_agg.vm_key + WHERE agg.vm_key IS NULL + AND COALESCE(lifecycle.deleted_at, prev_agg.deletion_time, 0) > 0 + AND COALESCE(lifecycle.deleted_at, prev_agg.deletion_time) > ? AND COALESCE(lifecycle.deleted_at, prev_agg.deletion_time) < ? +), agg_presence AS ( SELECT agg.vm_key, @@ -1204,6 +1267,16 @@ agg_presence AS ( %s AS presence FROM agg LEFT JOIN lifecycle ON lifecycle.vm_key = agg.vm_key + UNION ALL + SELECT + missing_deleted.vm_key, + missing_deleted."VcpuCount", + missing_deleted."RamGB", + missing_deleted.pool, + missing_deleted.creation_time, + missing_deleted.deletion_time, + missing_deleted.presence + FROM missing_deleted ), diag AS ( SELECT @@ -1253,7 +1326,7 @@ SELECT agg_diag.deleted_in_interval, agg_diag.partial_presence FROM diag, agg_diag -`, vmKeyExpr, overlapExpr, selected.TableName, templateExclusionFilter(), aggOverlapExpr) +`, vmKeyExpr, overlapExpr, selected.TableName, templateExclusionFilter(), vmKeyExpr, prevTableName, templateExclusionFilter(), aggOverlapExpr, aggOverlapExpr) query = dbConn.Rebind(query) var row struct { VmCount int64 `db:"vm_count"` @@ -1280,20 +1353,20 @@ FROM diag, agg_diag DeletedInInterval int64 `db:"deleted_in_interval"` PartialPresence int64 `db:"partial_presence"` } - args := []interface{}{ + overlapArgs := []interface{}{ hourEndUnix, hourEndUnix, hourStartUnix, hourStartUnix, hourEndUnix, hourEndUnix, hourStartUnix, hourStartUnix, durationSeconds, - hourEndUnix, hourEndUnix, - hourStartUnix, hourStartUnix, - hourEndUnix, hourEndUnix, - hourStartUnix, hourStartUnix, - durationSeconds, - hourStartUnix, hourEndUnix, - hourStartUnix, hourEndUnix, } + args := make([]interface{}, 0, len(overlapArgs)*3+6) + args = append(args, overlapArgs...) + args = append(args, overlapArgs...) + args = append(args, hourStartUnix, hourEndUnix) + args = append(args, overlapArgs...) + args = append(args, hourStartUnix, hourEndUnix) + args = append(args, hourStartUnix, hourEndUnix) if err := dbConn.GetContext(ctx, &row, query, args...); err != nil { return nil, err }