Skip to content

Recipient query and model#72

Merged
kentcdodds merged 9 commits into
mainfrom
cursor/recipient-query-and-model-2a28
Feb 2, 2026
Merged

Recipient query and model#72
kentcdodds merged 9 commits into
mainfrom
cursor/recipient-query-and-model-2a28

Conversation

@kentcdodds
Copy link
Copy Markdown
Member

@kentcdodds kentcdodds commented Feb 2, 2026

Summary

This PR addresses two key issues:

  1. Query Performance: The sendNextTexts cron query was slow (~259ms) due to inefficient SQLite patterns (LEFT JOIN, OR ... IS NULL, missing composite indexes). The query has been rewritten using raw SQL, INNER JOIN, and EXISTS, and supported by new composite and partial indexes. The OR ... IS NULL condition was eliminated by introducing a sentinel date.
  2. Data Model Clarity: The ambiguity of NULL values for Recipient.nextScheduledAt (meaning "unscheduled" or "invalid cron") has been resolved by replacing them with a SCHEDULE_SENTINEL_DATE (9999-12-31). This improves index efficiency and simplifies application logic.

The optimized query now uses indexes effectively, as confirmed by EXPLAIN QUERY PLAN, and all related application logic has been updated to handle the sentinel date.

Test Plan

  1. Run npm test to ensure all existing tests pass, verifying functional correctness.
  2. (Manual verification) Run EXPLAIN QUERY PLAN on the sendNextTexts query to confirm SEARCH USING INDEX on Recipient_cron_query_idx and User_stripe_active_idx.

Checklist

  • Tests updated
  • Docs updated

Screenshots

N/A


Open in Cursor Open in Web


Note

Medium Risk
Touches cron message delivery and introduces a data migration/backfill plus new index strategy; mistakes could skip sends or cause unnecessary schedule updates, but changes are localized and guarded with sentinel handling.

Overview
Improves cron-driven message sending performance by replacing the Prisma findMany + OR ... IS NULL recipient fetch with a TypedSQL query (getrecipientsforcron) backed by a new composite index, and by relying on denormalized Recipient.lastSentAt rather than aggregating from Message.

Introduces schedule sentinel dates (PREV_SCHEDULE_SENTINEL_DATE/NEXT_SCHEDULE_SENTINEL_DATE) to replace NULL schedule fields, updates create/edit/send/backfill and recipients list logic to write/recognize these values, and adds a migration to backfill NULLs and swap indexes. Tests and benchmarking are updated to populate and respect the cached schedule fields.

Written by Cursor Bugbot for commit d5a246c. This will update automatically on new commits. Configure here.

cursoragent and others added 5 commits February 2, 2026 14:14
…query

- Add SCHEDULE_SENTINEL_DATE constant (9999-12-31) to replace NULL nextScheduledAt
- Create migration to:
  - Drop redundant indexes
  - Update existing NULL values to sentinel date
  - Create optimized composite index for cron query
  - Create partial index on User for stripeId IS NOT NULL

Co-authored-by: me <me@kentcdodds.com>
- Replace Prisma findMany with optimized raw SQL using INNER JOIN
- Remove OR ... IS NULL condition that defeated index usage
- Add sentinel date check to schedule window validation
- Update SQL file with optimized query for TypedSQL
- Update schema to reflect new composite index

Co-authored-by: me <me@kentcdodds.com>
- Update text.server.ts to set sentinel date when schedule can't be computed
- Update __editor.server.tsx to use sentinel date for invalid cron expressions
- Update _layout.tsx to handle sentinel dates in schedule display

Co-authored-by: me <me@kentcdodds.com>
- Update backfill script to set sentinel date instead of NULL
- Update benchmark script to use optimized raw SQL query
- Both scripts now handle sentinel dates correctly

Co-authored-by: me <me@kentcdodds.com>
- Update createRecipient to compute prevScheduledAt/nextScheduledAt
- Update cron tests to use consistent schedule data
- Ensures tests work with optimized query that requires schedule fields

Co-authored-by: me <me@kentcdodds.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented Feb 2, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@kentcdodds kentcdodds marked this pull request as ready for review February 2, 2026 14:57
Comment thread app/utils/cron.server.ts Outdated
// 3. Handles both regular scheduled recipients and those with sentinel dates
// Note: SCHEDULE_SENTINEL_DATE (9999-12-31) will always be > reminderCutoff,
// so recipients with invalid schedules won't be included unless we explicitly fetch them
const recipients = await prisma.$queryRaw<CronRecipient[]>`
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursor please use prisma's typed sql feature for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web

Comment thread app/utils/cron.server.ts Outdated
await prisma.recipient.update({
where: { id: recipient.id },
data: {
prevScheduledAt: SCHEDULE_SENTINEL_DATE,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursor, I'm not sure, but I feel like the sentinel for previous should be far in the past

Copy link
Copy Markdown

@cursor cursor Bot Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The migration confirms it uses:

  • prevScheduledAt'1970-01-01T00:00:00.000Z' (far past)
  • nextScheduledAt'9999-12-31T23:59:59.999Z' (far future)

This change was addressed in commit b20519c which I pushed earlier. The logic makes sense:

  • prevScheduledAt in the far past means "there was never a valid previous schedule"
  • nextScheduledAt in the far future means "not scheduled" (won't be picked up by the <= reminderCutoff query)

Open in Cursor Open in Web

Comment thread scripts/benchmark-performance.ts Outdated
})

// Use optimized raw SQL query (same as production cron.server.ts)
const rawRecipients = await prisma.$queryRaw<CronBenchRecipient[]>`
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursor please use prisma's typed sql feature for this

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taking a look!

Open in Cursor Open in Web

Comment thread app/utils/cron.server.test.ts
Comment thread app/utils/schedule-constants.server.ts Outdated
Comment thread app/routes/_app+/recipients+/_layout.tsx Outdated
cursoragent and others added 2 commits February 2, 2026 15:07
- Replace $queryRaw with $queryRawTyped using getrecipientsforcron TypedSQL
- Add PREV_SCHEDULE_SENTINEL_DATE (1970-01-01) for far-past sentinel
- Add NEXT_SCHEDULE_SENTINEL_DATE (9999-12-31) for far-future sentinel
- Update migration to use correct sentinel dates for each field
- Update all code to use appropriate sentinel dates

Co-authored-by: me <me@kentcdodds.com>
Co-authored-by: me <me@kentcdodds.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented Feb 2, 2026

Bugbot Autofix prepared fixes for 3 of the 3 bugs found in the latest run.

  • ✅ Fixed: Test missing stripeId passes for wrong reason
    • Added a stripeId to the overdue-cron test user so the recipient is included in the query and the overdue logic is exercised.
  • ✅ Fixed: Unused exported helper functions
    • Removed the unused sentinel helper exports, leaving only the constants that are actually referenced.
  • ✅ Fixed: Incomplete sentinel check misses prevScheduledAt inconsistency
    • Expanded the sentinel update guard to also validate prevScheduledAt against its sentinel value, ensuring both fields stay consistent.

- Document lastSentAt denormalization in schema (avoids slow MAX() aggregate)
- Document sentinel dates in schema (avoids OR ... IS NULL pattern)
- Add comprehensive performance notes to getrecipientsforcron.sql
- Add comments in text.server.ts and schedule-constants.server.ts
- These comments prevent future developers from accidentally undoing optimizations

Co-authored-by: me <me@kentcdodds.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

Comment thread app/utils/cron.server.test.ts Outdated
Co-authored-by: me <me@kentcdodds.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented Feb 2, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Test no longer validates "too overdue" logic
    • Adjusted the test to set a recent next schedule and an older previous schedule so the overdue check is exercised while still matching the cron query window.

@kentcdodds kentcdodds merged commit 3166575 into main Feb 2, 2026
6 checks passed
@kentcdodds kentcdodds deleted the cursor/recipient-query-and-model-2a28 branch February 2, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants