refactor(scheduling): batch queries and DRY retention filter

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-22 00:57:51 +01:00
co-authored by Sisyphus
parent 1485b55d92
commit bdee60d34f
3 changed files with 68 additions and 86 deletions
+24 -8
View File
@@ -92,17 +92,33 @@ func UpdateDefaultHours(w http.ResponseWriter, r *http.Request) {
} }
defer tx.Rollback(r.Context()) defer tx.Rollback(r.Context())
for _, h := range hours { weekdays := make([]int, len(hours))
_, err := tx.Exec(r.Context(), ` startTimes := make([]string, len(hours))
UPDATE working_hours endTimes := make([]string, len(hours))
SET start_time=$1,end_time=$2,is_open=$3 isOpenFlags := make([]bool, len(hours))
WHERE weekday=$4 for i, h := range hours {
`, h.StartTime, h.EndTime, h.IsOpen, h.Weekday) weekdays[i] = h.Weekday
if err != nil { startTimes[i] = h.StartTime
endTimes[i] = h.EndTime
isOpenFlags[i] = h.IsOpen
}
if _, err := tx.Exec(r.Context(), `
UPDATE working_hours AS wh
SET start_time = v.start_time,
end_time = v.end_time,
is_open = v.is_open
FROM (
SELECT unnest($1::smallint[]) AS weekday,
unnest($2::time[]) AS start_time,
unnest($3::time[]) AS end_time,
unnest($4::boolean[]) AS is_open
) v
WHERE wh.weekday = v.weekday
`, weekdays, startTimes, endTimes, isOpenFlags); err != nil {
http.Error(w, "failed to update default hours", http.StatusInternalServerError) http.Error(w, "failed to update default hours", http.StatusInternalServerError)
return return
} }
}
if err := tx.Commit(r.Context()); err != nil { if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "failed to commit", http.StatusInternalServerError) http.Error(w, "failed to commit", http.StatusInternalServerError)
@@ -218,16 +218,13 @@ func CreateExceptionalGroup(w http.ResponseWriter, r *http.Request) {
} }
// Insert applications // Insert applications
for _, weekStart := range parsedWeeks { if _, err := tx.Exec(r.Context(), `
_, err := tx.Exec(r.Context(), `
INSERT INTO exceptional_group_applications (group_id, week_start) INSERT INTO exceptional_group_applications (group_id, week_start)
VALUES ($1, $2) SELECT $1, unnest($2::date[])
`, g.ID, weekStart) `, g.ID, parsedWeeks); err != nil {
if err != nil { http.Error(w, "failed to insert applications", http.StatusInternalServerError)
http.Error(w, "failed to insert application", http.StatusInternalServerError)
return return
} }
}
if err := tx.Commit(r.Context()); err != nil { if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "failed to commit transaction", http.StatusInternalServerError) http.Error(w, "failed to commit transaction", http.StatusInternalServerError)
@@ -328,16 +325,13 @@ func UpdateExceptionalApplications(w http.ResponseWriter, r *http.Request) {
} }
// Insert new applications // Insert new applications
for _, weekStart := range parsedWeeks { if _, err := tx.Exec(r.Context(), `
_, err := tx.Exec(r.Context(), `
INSERT INTO exceptional_group_applications (group_id, week_start) INSERT INTO exceptional_group_applications (group_id, week_start)
VALUES ($1, $2) SELECT $1, unnest($2::date[])
`, req.GroupID, weekStart) `, req.GroupID, parsedWeeks); err != nil {
if err != nil { http.Error(w, "failed to insert applications", http.StatusInternalServerError)
http.Error(w, "failed to insert application", http.StatusInternalServerError)
return return
} }
}
if err := tx.Commit(r.Context()); err != nil { if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "failed to commit transaction", http.StatusInternalServerError) http.Error(w, "failed to commit transaction", http.StatusInternalServerError)
+29 -57
View File
@@ -197,12 +197,11 @@ func DeleteTimeBlocker(w http.ResponseWriter, r *http.Request) {
// Returns blockers for the given date range, expanded for recurring blockers // Returns blockers for the given date range, expanded for recurring blockers
// Used by GetAvailableHours to subtract blocked time from available slots // Used by GetAvailableHours to subtract blocked time from available slots
func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBlocker, error) { func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBlocker, error) {
// Get one-off blockers in range
rows, err := db.Conn.Query(ctx, ` rows, err := db.Conn.Query(ctx, `
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
FROM time_blockers FROM time_blockers
WHERE cron_expression IS NULL WHERE (cron_expression IS NULL AND start_time >= $1 AND start_time <= $2)
AND start_time >= $1 AND start_time <= $2 OR (cron_expression IS NOT NULL)
ORDER BY start_time ORDER BY start_time
`, start, end) `, start, end)
if err != nil { if err != nil {
@@ -216,30 +215,15 @@ func GetTimeBlockersInRange(ctx context.Context, start, end time.Time) ([]TimeBl
if err := rows.Scan(&b.ID, &b.StartTime, &b.DurationMinutes, &b.Description, &b.CronExpression, &b.CreatedAt, &b.CreatedBy); err != nil { if err := rows.Scan(&b.ID, &b.StartTime, &b.DurationMinutes, &b.Description, &b.CronExpression, &b.CreatedAt, &b.CreatedBy); err != nil {
return nil, err return nil, err
} }
blockers = append(blockers, b) if b.CronExpression != nil {
}
rows.Close()
// Get ALL recurring blockers and expand them
recurringRows, err := db.Conn.Query(ctx, `
SELECT id, start_time, duration_minutes, description, cron_expression, created_at, created_by
FROM time_blockers
WHERE cron_expression IS NOT NULL
`)
if err != nil {
return nil, err
}
defer recurringRows.Close()
for recurringRows.Next() {
var b TimeBlocker
if err := recurringRows.Scan(&b.ID, &b.StartTime, &b.DurationMinutes, &b.Description, &b.CronExpression, &b.CreatedAt, &b.CreatedBy); err != nil {
return nil, err
}
// Expand recurring blocker to occurrences within range
occurrences := expandCronOccurrences(b, start, end) occurrences := expandCronOccurrences(b, start, end)
blockers = append(blockers, occurrences...) blockers = append(blockers, occurrences...)
} else {
blockers = append(blockers, b)
}
}
if err := rows.Err(); err != nil {
return nil, err
} }
return blockers, nil return blockers, nil
@@ -459,6 +443,14 @@ func CleanupExpiredLoyaltyRedemptions(ctx context.Context) error {
// - Anonymized users: 7 years from payment creation AND 1 year since anonymization // - Anonymized users: 7 years from payment creation AND 1 year since anonymization
// //
// The function is idempotent — running it twice produces the same result. // The function is idempotent — running it twice produces the same result.
const retentionFilter = `AND (
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)`
func CleanupExpiredFinancialRecords(ctx context.Context) error { func CleanupExpiredFinancialRecords(ctx context.Context) error {
tx, err := db.Conn.Begin(ctx) tx, err := db.Conn.Begin(ctx)
if err != nil { if err != nil {
@@ -486,12 +478,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN bookings b ON p.booking_id = b.id LEFT JOIN bookings b ON p.booking_id = b.id
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE p.created_at < NOW() - INTERVAL '7 years' WHERE p.created_at < NOW() - INTERVAL '7 years'
AND ( ` + retentionFilter + `
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
GROUP BY DATE_TRUNC('month', p.created_at)::date GROUP BY DATE_TRUNC('month', p.created_at)::date
ON CONFLICT (month) DO UPDATE SET ON CONFLICT (month) DO UPDATE SET
total_payments = financial_aggregates.total_payments + EXCLUDED.total_payments, total_payments = financial_aggregates.total_payments + EXCLUDED.total_payments,
@@ -521,12 +508,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN bookings b ON p.booking_id = b.id LEFT JOIN bookings b ON p.booking_id = b.id
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE p.created_at < NOW() - INTERVAL '7 years' WHERE p.created_at < NOW() - INTERVAL '7 years'
AND ( ` + retentionFilter + `
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
GROUP BY DATE_TRUNC('month', r.created_at)::date GROUP BY DATE_TRUNC('month', r.created_at)::date
ON CONFLICT (month) DO UPDATE SET ON CONFLICT (month) DO UPDATE SET
total_refunds = financial_aggregates.total_refunds + EXCLUDED.total_refunds total_refunds = financial_aggregates.total_refunds + EXCLUDED.total_refunds
@@ -541,12 +523,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE p.booking_id = b.id WHERE p.booking_id = b.id
AND p.created_at < NOW() - INTERVAL '7 years' AND p.created_at < NOW() - INTERVAL '7 years'
AND ( ` + retentionFilter + `
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
`) `)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete expired payments: %w", err) return fmt.Errorf("failed to delete expired payments: %w", err)
@@ -559,12 +536,7 @@ func CleanupExpiredFinancialRecords(ctx context.Context) error {
LEFT JOIN users u ON b.user_id = u.id LEFT JOIN users u ON b.user_id = u.id
WHERE r.payment_id = p.id WHERE r.payment_id = p.id
AND p.created_at < NOW() - INTERVAL '7 years' AND p.created_at < NOW() - INTERVAL '7 years'
AND ( ` + retentionFilter + `
b.user_id IS NULL
OR u.id IS NULL
OR NOT (u.account_role = 'guest' AND (u.email LIKE 'anon-%@anon.invalid' OR u.email LIKE 'deleted+%@deleted.invalid'))
OR u.updated_at < NOW() - INTERVAL '1 year'
)
`) `)
if err != nil { if err != nil {
return fmt.Errorf("failed to delete expired refunds: %w", err) return fmt.Errorf("failed to delete expired refunds: %w", err)
@@ -867,10 +839,10 @@ func CleanupIdleAccounts(ctx context.Context) error {
return fmt.Errorf("failed to batch zero account balances: %w", err) return fmt.Errorf("failed to batch zero account balances: %w", err)
} }
for _, id := range ids { if _, err = tx.Exec(ctx, `
if _, err = tx.Exec(ctx, "SELECT anonymize_user($1)", id); err != nil { SELECT anonymize_user(unnest($1::text[]))
return fmt.Errorf("failed to anonymize idle account %s: %w", id, err) `, ids); err != nil {
} return fmt.Errorf("failed to anonymize idle accounts: %w", err)
} }
} }
@@ -899,10 +871,10 @@ func CleanupIdleAccounts(ctx context.Context) error {
} }
rowsNoBalance.Close() rowsNoBalance.Close()
for _, id := range accountsNoBalance { if _, err = tx.Exec(ctx, `
if _, err = tx.Exec(ctx, "SELECT anonymize_user($1)", id); err != nil { SELECT anonymize_user(unnest($1::text[]))
return fmt.Errorf("failed to anonymize idle account %s: %w", id, err) `, accountsNoBalance); err != nil {
} return fmt.Errorf("failed to anonymize idle accounts: %w", err)
} }
return tx.Commit(ctx) return tx.Commit(ctx)