fix(reminder): inline offset, drop unused $2 in evening query
$2 was the offset, used only in the morning dateCond. Evening's query
referenced $1, $3, $4 — $2 was passed but unused, and Postgres can't
infer the type of an unreferenced parameter ('could not determine data
type of parameter $2', 42P18).
Inline offset directly into the morning dateCond as a literal '%d days'
(safe — it's clamped to ≥1 above). New positional layout:
$1 = today
$2 = userid
$3 = is_global_admin
Three rounds of SQL fights for one query. Adding integration coverage
to TestRunSlotForUser is a follow-up — it currently skips when
TEST_DATABASE_URL is unset, which is why none of these reached prod via
CI.
This commit is contained in:
@@ -226,22 +226,25 @@ func (s *ReminderService) fetchSlotDeadlines(ctx context.Context, u models.User,
|
||||
}
|
||||
|
||||
// Build the date predicate per slot. Positional placeholders only —
|
||||
// sqlx.Named cannot be used here because the query body contains
|
||||
// PostgreSQL `::TYPE` cast operators (`::uuid[]`, `::date`, `::interval`)
|
||||
// and sqlx eats the second `:` thinking it's a named-arg prefix.
|
||||
// sqlx.Named can't be used because the query body contains PostgreSQL
|
||||
// `::TYPE` cast operators and sqlx eats the second `:` as a named-arg
|
||||
// prefix.
|
||||
// $1 = today
|
||||
// $2 = offset (days)
|
||||
// $3 = userid
|
||||
// $4 = is_global_admin
|
||||
// $2 = userid
|
||||
// $3 = is_global_admin
|
||||
// `offset` is interpolated as a literal int (clamped ≥1 above) — keeping
|
||||
// it as a parameter would force every slot's query to declare it even
|
||||
// when unused (evening), and Postgres can't infer the type of an
|
||||
// unreferenced parameter.
|
||||
// morning: overdue OR due_today OR due_warning(today+offset)
|
||||
// evening: overdue OR due_today (no +offset heads-up in the evening)
|
||||
var dateCond string
|
||||
if slot == "evening" {
|
||||
dateCond = `(f.due_date < $1 OR f.due_date = $1)`
|
||||
} else {
|
||||
dateCond = `(f.due_date < $1
|
||||
dateCond = fmt.Sprintf(`(f.due_date < $1
|
||||
OR f.due_date = $1
|
||||
OR f.due_date = ($1::date + ($2 || ' days')::interval)::date)`
|
||||
OR f.due_date = ($1::date + '%d days'::interval)::date)`, offset)
|
||||
}
|
||||
|
||||
// Audience predicates:
|
||||
@@ -262,7 +265,7 @@ func (s *ReminderService) fetchSlotDeadlines(ctx context.Context, u models.User,
|
||||
p.title AS project_title,
|
||||
EXISTS (
|
||||
SELECT 1 FROM paliad.project_teams pt
|
||||
WHERE pt.user_id = $3
|
||||
WHERE pt.user_id = $2
|
||||
AND pt.role = 'lead'
|
||||
AND pt.project_id = ANY(string_to_array(p.path, '.')::uuid[])
|
||||
) AS is_lead
|
||||
@@ -272,20 +275,20 @@ func (s *ReminderService) fetchSlotDeadlines(ctx context.Context, u models.User,
|
||||
WHERE f.status = 'pending'
|
||||
AND ` + dateCond + `
|
||||
AND (
|
||||
f.created_by = $3
|
||||
f.created_by = $2
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM paliad.project_teams pt
|
||||
WHERE pt.user_id = $3
|
||||
WHERE pt.user_id = $2
|
||||
AND pt.role = 'lead'
|
||||
AND pt.project_id = ANY(string_to_array(p.path, '.')::uuid[])
|
||||
)
|
||||
OR own.escalation_contact_id = $3
|
||||
OR ($4 = TRUE AND own.escalation_contact_id IS NULL)
|
||||
OR own.escalation_contact_id = $2
|
||||
OR ($3 = TRUE AND own.escalation_contact_id IS NULL)
|
||||
)
|
||||
ORDER BY f.due_date ASC, f.id ASC`
|
||||
|
||||
rows := []digestRow{}
|
||||
if err := s.db.SelectContext(ctx, &rows, query, today, offset, u.ID, isGlobalAdmin); err != nil {
|
||||
if err := s.db.SelectContext(ctx, &rows, query, today, u.ID, isGlobalAdmin); err != nil {
|
||||
return nil, fmt.Errorf("select deadlines: %w", err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user