From d5b5cb48938c546c8623164e7a77478586195001 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 18 Feb 2025 08:58:12 -0500 Subject: [PATCH] break intervals out --- internal/common/intervals.go | 84 +++++++++++++++++++++++++++++++ pkg/database/partman/intervals.go | 73 +++++++++------------------ pkg/database/partman/partman.go | 20 +------- pkg/database/stats.sql.go | 4 +- pkg/stats/stats.go | 13 ++++- sql/postgres/queries/stats.sql | 4 +- 6 files changed, 125 insertions(+), 73 deletions(-) create mode 100644 internal/common/intervals.go diff --git a/internal/common/intervals.go b/internal/common/intervals.go new file mode 100644 index 0000000..0458f3c --- /dev/null +++ b/internal/common/intervals.go @@ -0,0 +1,84 @@ +package common + +import ( + "time" +) + +const ( + DaysInWeek = 7 + MonthsInQuarter = 3 +) + +type TimeBounder interface { + GetDailyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetWeeklyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetMonthlyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetQuarterlyBounds(date time.Time) (lowerBound, upperBound time.Time) + GetYearlyBounds(date time.Time) (lowerBound, upperBound time.Time) +} + +type tbOpt func(*timeBounder) + +func WithLocation(l *time.Location) tbOpt { + return func(tb *timeBounder) { + tb.loc = l + } +} + +func NewTimeBounder(opts ...tbOpt) timeBounder { + tb := timeBounder{} + + for _, opt := range opts { + opt(&tb) + } + + if tb.loc == nil { + tb.loc = time.UTC + } + + return tb +} + +type timeBounder struct { + loc *time.Location +} + +func (tb timeBounder) GetDailyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(0, 0, 1) + + return +} + +func (tb timeBounder) GetWeeklyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, tb.loc).AddDate(0, 0, -int(date.Weekday()-time.Monday)) + upperBound = lowerBound.AddDate(0, 0, DaysInWeek) + + return +} + +func (tb timeBounder) GetMonthlyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(0, 1, 0) + + return +} + +func (tb *timeBounder) GetQuarterlyBounds(date time.Time) (lowerBound, upperBound time.Time) { + year, _, _ := date.Date() + + quarter := (int(date.Month()) - 1) / MonthsInQuarter + firstMonthOfTheQuarter := time.Month(quarter*MonthsInQuarter + 1) + + lowerBound = time.Date(year, firstMonthOfTheQuarter, 1, 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(0, MonthsInQuarter, 0) + + return +} + +func (tb timeBounder) GetYearlyBounds(date time.Time) (lowerBound, upperBound time.Time) { + lowerBound = time.Date(date.Year(), 1, 1, 0, 0, 0, 0, tb.loc) + upperBound = lowerBound.AddDate(1, 0, 0) + + return +} diff --git a/pkg/database/partman/intervals.go b/pkg/database/partman/intervals.go index 0252302..e040b02 100644 --- a/pkg/database/partman/intervals.go +++ b/pkg/database/partman/intervals.go @@ -3,66 +3,23 @@ package partman import ( "fmt" "time" + + "dynatron.me/x/stillbox/internal/common" ) -const ( - daysInWeek = 7 - monthsInQuarter = 3 -) - -func getDailyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(0, 0, 1) - - return -} - -func getWeeklyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC).AddDate(0, 0, -int(date.Weekday()-time.Monday)) - upperBound = lowerBound.AddDate(0, 0, daysInWeek) - - return -} - -func getMonthlyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(0, 1, 0) - - return -} - -func getQuarterlyBounds(date time.Time) (lowerBound, upperBound time.Time) { - year, _, _ := date.Date() - - quarter := (int(date.Month()) - 1) / monthsInQuarter - firstMonthOfTheQuarter := time.Month(quarter*monthsInQuarter + 1) - - lowerBound = time.Date(year, firstMonthOfTheQuarter, 1, 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(0, monthsInQuarter, 0) - - return -} - -func getYearlyBounds(date time.Time) (lowerBound, upperBound time.Time) { - lowerBound = time.Date(date.Year(), 1, 1, 0, 0, 0, 0, time.UTC) - upperBound = lowerBound.AddDate(1, 0, 0) - - return -} - func (p Partition) Next(i int) Partition { var t time.Time switch p.Interval { case Daily: t = p.Time.AddDate(0, 0, i) case Weekly: - t = p.Time.AddDate(0, 0, i*daysInWeek) + t = p.Time.AddDate(0, 0, i*common.DaysInWeek) case Monthly: year, month, _ := p.Time.Date() t = time.Date(year, month+time.Month(i), 1, 0, 0, 0, 0, p.Time.Location()) case Quarterly: - t = p.Time.AddDate(0, i*monthsInQuarter, 0) + t = p.Time.AddDate(0, i*common.MonthsInQuarter, 0) case Yearly: year, _, _ := p.Time.Date() @@ -125,13 +82,13 @@ func (p Partition) Prev(i int) Partition { case Daily: t = p.Time.AddDate(0, 0, -i) case Weekly: - t = p.Time.AddDate(0, 0, -i*daysInWeek) + t = p.Time.AddDate(0, 0, -i*common.DaysInWeek) case Monthly: year, month, _ := p.Time.Date() t = time.Date(year, month-time.Month(i), 1, 0, 0, 0, 0, p.Time.Location()) case Quarterly: - t = p.Time.AddDate(0, -i*monthsInQuarter, 0) + t = p.Time.AddDate(0, -i*common.MonthsInQuarter, 0) case Yearly: year, _, _ := p.Time.Date() @@ -150,3 +107,21 @@ func (p Partition) Prev(i int) Partition { return pp } + +func (p Partition) Range() (time.Time, time.Time) { + b := common.NewTimeBounder() + switch p.Interval { + case Daily: + return b.GetDailyBounds(p.Time) + case Weekly: + return b.GetWeeklyBounds(p.Time) + case Monthly: + return b.GetMonthlyBounds(p.Time) + case Quarterly: + return b.GetQuarterlyBounds(p.Time) + case Yearly: + return b.GetYearlyBounds(p.Time) + } + + panic("unknown interval!") +} diff --git a/pkg/database/partman/partman.go b/pkg/database/partman/partman.go index 9b7afcd..461d67b 100644 --- a/pkg/database/partman/partman.go +++ b/pkg/database/partman/partman.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/isoweek" "dynatron.me/x/stillbox/pkg/config" "dynatron.me/x/stillbox/pkg/database" @@ -325,23 +326,6 @@ func (pm *partman) Check(ctx context.Context, now time.Time) error { }, pgx.TxOptions{}) } -func (p Partition) Range() (time.Time, time.Time) { - switch p.Interval { - case Daily: - return getDailyBounds(p.Time) - case Weekly: - return getWeeklyBounds(p.Time) - case Monthly: - return getMonthlyBounds(p.Time) - case Quarterly: - return getQuarterlyBounds(p.Time) - case Yearly: - return getYearlyBounds(p.Time) - } - - panic("unknown interval!") -} - func (p Partition) PartitionName() string { return p.Name } @@ -423,7 +407,7 @@ func (pm *partman) verifyPartName(pr database.PartitionResult) (p Partition, err if quarterNum > 4 { return p, PartitionError(pn, errors.New("invalid quarter")) } - firstMonthOfTheQuarter := time.Month((quarterNum-1)*monthsInQuarter + 1) + firstMonthOfTheQuarter := time.Month((quarterNum-1)*common.MonthsInQuarter + 1) parsed := time.Date(year, firstMonthOfTheQuarter, 1, 0, 0, 0, 0, time.UTC) if parsed != p.Time { return p, PartitionError(pn, ParsedIntvlErr{parsed: parsed, start: p.Time}) diff --git a/pkg/database/stats.sql.go b/pkg/database/stats.sql.go index 3db6ae8..6a54683 100644 --- a/pkg/database/stats.sql.go +++ b/pkg/database/stats.sql.go @@ -20,7 +20,7 @@ WHERE CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= $2 ELSE TRUE END AND CASE WHEN $3::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= $3 ELSE TRUE END + c.call_date < $3 ELSE TRUE END GROUP BY date ORDER BY date DESC ` @@ -61,7 +61,7 @@ WHERE CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= $2 ELSE TRUE END AND CASE WHEN $3::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= $3 ELSE TRUE END + c.call_date < $3 ELSE TRUE END GROUP BY 2, 3, 4 ORDER BY 4 DESC ` diff --git a/pkg/stats/stats.go b/pkg/stats/stats.go index b1e03bd..e8c4d31 100644 --- a/pkg/stats/stats.go +++ b/pkg/stats/stats.go @@ -5,10 +5,12 @@ import ( "time" "dynatron.me/x/stillbox/internal/cache" + "dynatron.me/x/stillbox/internal/common" "dynatron.me/x/stillbox/internal/jsontypes" "dynatron.me/x/stillbox/pkg/calls" "dynatron.me/x/stillbox/pkg/calls/callstore" "dynatron.me/x/stillbox/pkg/services" + "github.com/rs/zerolog/log" ) const DefaultExpiration = 5 * time.Minute @@ -56,20 +58,27 @@ func (s *stats) GetCallStats(ctx context.Context, interval calls.StatsInterval) var start time.Time now := time.Now() + end := now + bnd := common.NewTimeBounder(common.WithLocation(now.Location())) + switch interval { case calls.IntervalHour: start = now.Add(-24 * time.Hour) // one day case calls.IntervalDay: start = now.Add(-7 * 24 * time.Hour) // one week case calls.IntervalWeek: - start = now.Add(-30 * 24 * time.Hour) // one month + start, end = bnd.GetMonthlyBounds(now) + start, _ = bnd.GetWeeklyBounds(start) + _, end = bnd.GetWeeklyBounds(end) case calls.IntervalMonth: start = now.Add(-365 * 24 * time.Hour) // one year default: return nil, calls.ErrInvalidInterval } - st, err := s.cs.CallStats(ctx, interval, jsontypes.Time(start), jsontypes.Time(now)) + log.Debug().Str("start", start.String()).Str("end", end.String()).Msg("bound") + + st, err := s.cs.CallStats(ctx, interval, jsontypes.Time(start), jsontypes.Time(end)) if err != nil { return nil, err } diff --git a/sql/postgres/queries/stats.sql b/sql/postgres/queries/stats.sql index e201da8..32538c7 100644 --- a/sql/postgres/queries/stats.sql +++ b/sql/postgres/queries/stats.sql @@ -9,7 +9,7 @@ WHERE CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= @start ELSE TRUE END AND CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= sqlc.narg('end') ELSE TRUE END + c.call_date < sqlc.narg('end') ELSE TRUE END GROUP BY 2, 3, 4 ORDER BY 4 DESC; @@ -22,6 +22,6 @@ WHERE CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN c.call_date >= @start ELSE TRUE END AND CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN - c.call_date <= sqlc.narg('end') ELSE TRUE END + c.call_date < sqlc.narg('end') ELSE TRUE END GROUP BY date ORDER BY date DESC;