Skip to content

Make attendee reads faster and load them all one way - #1790

Merged
stefan-burke merged 8 commits into
mainfrom
claude/admin-homepage-queries-el4dr0
Jul 12, 2026
Merged

Make attendee reads faster and load them all one way#1790
stefan-burke merged 8 commits into
mainfrom
claude/admin-homepage-queries-el4dr0

Conversation

@stefan-burke

@stefan-burke stefan-burke commented Jul 12, 2026

Copy link
Copy Markdown
Member

What this changes

The admin homepage was doing a lot of database work it didn't need. Every list of bookings — the dashboard's recent bookings, the attendees page, the calendar, the service-event list — ran the same heavy database query, even when the page only showed a booking's name, date and quantity. The most expensive parts of that query work out how much each person paid and what they still owe; those numbers take several extra lookups per booking and most pages never show them.

This change lets each page ask for only the booking details it actually shows, so the database skips the work behind anything the page won't display.

How it works now

There is now one shared way to load bookings. A page says which bookings it wants (by listing, by person, by package, upcoming ones, a date range, or a specific set of ids) and which details it needs, and the system builds the smallest query that answers that. Before, each page hand-wrote its own database query; there were lots of near-copies that could quietly drift apart. Now they all go through the same place.

Concretely:

  • The dashboard's recent bookings and the service-event list no longer work out payment amounts and balances they never show. That's the biggest saving — those were the slowest queries on the page.
  • Every booking list — the attendees page, the calendar, group and roster views, the ICS feed, the service-event pages, the CSV export, payment webhooks and emails — now loads through the single shared reader. Passing a list of ids is now a first-class option.
  • Because the filter is described once, two pages that want the same bookings but different details (for example the attendees table, which shows no money, and the CSV export, which does) can each ask for exactly what they need over the same filter.

Safety

Nothing a page displays has changed — where a page does show payment or balance figures, it still asks for them and still shows the same values. The one query that had no filter on the kind of booking keeps that behaviour. Every existing test passes, coverage stays at 100%, and the new shared query builder is covered by its own tests with a full mutation score.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added field-selective attendee data fetching to return only the columns needed for browsing and servicing.
  • Refactor
    • Standardized attendee loading across admin, listings, servicing, and reservation flows using shared SQL/query building.
    • Updated UI data shaping to use a display-only attendee type in the attendee table and admin dashboard.
  • Tests
    • Added unit tests for attendee SQL projection, join, filter, and ordering builders; updated related test utilities/imports.
  • Documentation
    • Added a TODO note about whether servicing events should appear on the logistics run sheet.

claude added 4 commits July 12, 2026 13:01
…ns so far)

Introduce src/shared/db/attendees/select.ts: one place that assembles the
attendee SELECT from a per-field opt-in list. It always emits the cheap
identity and per-listing columns and adds an expensive ledger subquery
(remaining_balance, refunded, price_paid) only when the caller asks for it.
The three subquery fragments move here from queries.ts (their true home now);
queries.ts, tokens.ts and balance.ts import them.

decryptAttendees/decryptAttendeeFields become generic over the selected row
shape, coercing price_paid/refunded only when those columns are actually
present so a narrowed read never fabricates a value.

Every caller of the old ATTENDEE_JOIN_SELECT / ATTENDEE_LEFT_JOIN_SELECT
constants now goes through the builder with the full field set, so this
commit changes no SQL output — it only puts the shared mechanism in place.
The two fat constants are deleted. Narrowing individual reads to the fields
they actually display comes next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq
…bqueries

The admin dashboard's recent-bookings table and the /admin/servicing summary
list show only a booking's quantity, date, check-in state and (dashboard)
refunded badge — never price_paid or remaining_balance. They were paying for
those correlated ledger subqueries on every line regardless.

Introduce DisplayAttendee — the exact attendee fields the shared attendee
table reads — and type AttendeeTableRow/the row builders/CheckinButton against
it. A full Attendee still satisfies it, and so does a field-selected read that
skipped the money columns. getNewestAttendeesRaw now asks for just `refunded`
(4 price_paid subqueries + remaining_balance dropped per line), and the
servicing summary read asks for no money fields at all.

The attendees-browser page query stays on the full field set for now because
the same query also backs the CSV export, which sums price_paid — the next
step centralises attendee reads behind one getAttendees(filter, fields) so the
table and the CSV can ask for different fields over the same filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq
…, fields)

Instead of each reader hand-writing its own SELECT/FROM/WHERE/ORDER, they now
declare WHICH attendees they want and WHICH fields to project, and one façade
builds the minimal query. `AttendeeWhere` covers every filter the old readers
used (by listing, by attendee id or id-list, by package group, real-lines-only,
upcoming-from-date, a daily-listing date range, and a "pick these attendee ids"
subquery for the newest/paged reads); `AttendeeOrder` names the orderings; and
`getAttendees` / `getAttendeeRow` / `attendeeBatchStatement` share one resolver.

Passing a list of ids is now first-class: `where: { attendeeIds }` and
`where: { listingIds }`. getAttendeesRaw, getAttendeePackageRowsRaw,
getNewestAttendeesRaw, getAttendeesPage, getAttendeeRaw, getAttendeesByIds,
getDailyListingAttendeesByDate, getAttendeesByListingIds, the two batch
listing+attendee readers, and both servicing readers all now go through it.
Because the filter is declared once, the browsing table and the CSV export can
ask for different fields over the same filter — the whole point.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq
…ws kind-agnostic

Add exact-string assertions for the generated column list and FROM/WHERE/ORDER
so a mutation to any column, separator, COALESCE wrapper, ledger subquery,
WHERE clause, join keyword or ORDER BY is caught — the builder's output is its
contract. The two defaulted `kind`/`join` operands are string unions that can
never be "", so `?? → ||` on them is provably equivalent and is recorded in
equivalent-mutants.txt. select.ts now mutation-scores 100% (90 killed, 2
equivalents).

Also restore getAttendeePackageRowsRaw's original behaviour: it never filtered
by kind (the attendee id already pins one attendee), so it now passes
`attendee-or-servicing` (every kind the CHECK allows) instead of the default
regular-attendee filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ecac060c-e55a-4517-9bb4-da25f352c338

📥 Commits

Reviewing files that changed from the base of the PR and between b896c8c and c0a7641.

📒 Files selected for processing (1)
  • src/shared/db/attendees/select.ts

📝 Walkthrough

Walkthrough

The attendee data-access layer now provides typed, field-selective SQL builders and shared filtering, ordering, and batch-query helpers. Attendee, servicing, listing, administrative, decryption, and display paths use these helpers, with generated-SQL tests and updated row types.

Changes

Attendee query and projection refactor

Layer / File(s) Summary
Typed attendee SELECT builders
src/shared/db/attendees/select.ts, test/lib/db/attendees/select.test.ts
Adds ledger projections, typed field selection, shared filters and ordering, batch statements, query execution, and SQL-generation tests.
Shared attendee reader migration
src/shared/db/attendees/queries.ts, src/shared/db/attendees/servicing.ts, src/shared/db/listings.ts, src/shared/db/attendees/{balance,tokens}.ts
Replaces inline attendee SQL with shared query APIs across browsing, paging, servicing, listing, daily-range, and batch reads.
Raw, decrypted, and display row types
src/shared/db/attendees/pii.ts, src/shared/types.ts, src/shared/attendee-table-rows.ts, src/ui/templates/*.tsx
Adds conditional raw/decrypted row typing and uses DisplayAttendee for table and dashboard rendering.
Administrative attendee loaders
src/features/admin/attendees-edit.ts, src/features/admin/attendees-merge.ts
Uses shared attendee readers for refresh-payment and merge-target loading.
Supporting validation and utilities
test/lib/server-reservation/*.ts, test/shared/db/attendees/servicing/code-quality.test.ts, test/test-utils/servicing.ts, scripts/mutation/equivalent-mutants.txt, TODO.md
Updates helper imports and servicing reuse assertions, records equivalent-mutant suppressions, and documents a servicing-event follow-up.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AttendeeReader
  participant getAttendees
  participant attendeeFromWhere
  participant Database
  participant decryptAttendees
  AttendeeReader->>getAttendees: fields, join, filters, and order
  getAttendees->>attendeeFromWhere: build projection and SQL tail
  getAttendees->>Database: execute SQL with bound arguments
  Database-->>getAttendees: raw selected attendee rows
  getAttendees-->>AttendeeReader: typed attendee rows
  AttendeeReader->>decryptAttendees: decrypt selected rows
  decryptAttendees-->>AttendeeReader: decrypted attendee rows
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: consolidating attendee reads behind one shared path to improve performance.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/admin-homepage-queries-el4dr0

Comment @coderabbitai help to get the list of available commands.

…-queries-el4dr0

# Conflicts:
#	scripts/mutation/equivalent-mutants.txt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/mutation/equivalent-mutants.txt`:
- Around line 899-903: Resolve the merge conflict in
scripts/mutation/equivalent-mutants.txt against origin/main, preserving the
equivalent-mutant suppression entries for getAttendees at
src/shared/db/attendees/select.ts lines 325 and 414. Remove all conflict
markers, verify the file contains the correct combined entries, and recommit the
resolved result.

In `@src/features/admin/attendees-edit.ts`:
- Around line 19-24: Replace the duplicated getAttendeeRow query construction in
both handlers with the existing getAttendeeRaw(attendeeId) helper from
`#shared/db/attendees/queries.ts`. Update imports and preserve each handler’s
existing result handling, removing the now-unused ATTENDEE_FIELDS and
getAttendeeRow imports.

In `@src/features/admin/attendees-merge.ts`:
- Around line 16-23: Update loadMergeTarget to reuse getAttendeeRaw(attendeeId)
from `#shared/db/attendees/queries.ts` instead of rebuilding the getAttendeeRow
call with ATTENDEE_FIELDS, the left join, and attendeeId filter. Add or adjust
the import accordingly and remove now-unused query-construction imports.

In `@src/shared/db/attendees/queries.ts`:
- Around line 388-395: Update getAttendeesByIds to explicitly disable the
default kind filter when calling getAttendees, keeping the attendee ID lookup
kind-agnostic so servicing run-sheet rows are included and loadLegLookups
receives all matching entries.

In `@src/shared/db/attendees/select.ts`:
- Around line 230-235: Update attendeeBatchStatement to reuse attendeeSql with
the { args, fields, from, join } result from resolveAttendeeQuery instead of
re-inlining the SELECT template. Preserve the existing query arguments and
ensure batch readers use the exact SQL columns and clauses produced by
attendeeSql.

In `@test/lib/db/attendees/select.test.ts`:
- Around line 124-144: Update the singular-id tests and their callers to use
only attendeeIds and listingIds, passing a one-element array for single lookups.
Remove expectations and setup for attendeeId/listingId, and ensure
attendeeFromWhere continues producing one placeholder with the same argument
value for single-item arrays.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b668e185-17e8-4180-83d5-ea1dea720511

📥 Commits

Reviewing files that changed from the base of the PR and between 45f46d9 and b189807.

📒 Files selected for processing (19)
  • scripts/mutation/equivalent-mutants.txt
  • src/features/admin/attendees-edit.ts
  • src/features/admin/attendees-merge.ts
  • src/shared/attendee-table-rows.ts
  • src/shared/db/attendees/balance.ts
  • src/shared/db/attendees/pii.ts
  • src/shared/db/attendees/queries.ts
  • src/shared/db/attendees/select.ts
  • src/shared/db/attendees/servicing.ts
  • src/shared/db/attendees/tokens.ts
  • src/shared/db/listings.ts
  • src/shared/types.ts
  • src/ui/templates/admin/dashboard.tsx
  • src/ui/templates/attendee-table.tsx
  • test/lib/db/attendees/select.test.ts
  • test/lib/server-reservation/deposit-basics.test.ts
  • test/lib/server-reservation/helpers.ts
  • test/shared/db/attendees/servicing/code-quality.test.ts
  • test/test-utils/servicing.ts

Comment thread scripts/mutation/equivalent-mutants.txt Outdated
Comment thread src/features/admin/attendees-edit.ts Outdated
Comment thread src/features/admin/attendees-merge.ts Outdated
Comment thread src/shared/db/attendees/queries.ts
Comment thread src/shared/db/attendees/select.ts Outdated
Comment thread test/lib/db/attendees/select.test.ts Outdated
…isfy stricter cpd

Main's tightened jscpd threshold (#1787) flagged the paired thin wrappers and
the duplicated attendee-by-id load once merged in. Restructure the reader layer
so there's a single SQL builder:

- attendeeBatchStatement is now the one place a declared query becomes SQL;
  getAttendees runs it, and the batch listing readers embed it. Removes the
  selectAttendees/selectAttendeeOrNull pair and getAttendeeRow.
- getAttendeeRaw is the single attendee-by-id read; attendees-edit and
  attendees-merge reuse it instead of re-inlining the same getAttendees call.
- Collapse the singular attendeeId/listingId filters into attendeeIds/listingIds
  (a single lookup is a one-element array), matching the repo's "one path for
  one-or-many" rule and dropping the duplicate WHERE branches.

getAttendeesByIds keeps its kind='attendee' filter — that's the exact
behaviour the pre-existing query had; whether the logistics run sheet should
include servicing events is a separate product decision, recorded in TODO.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/shared/db/attendees/select.ts (1)

291-306: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard empty attendeeIds / listingIds here. inList() still emits IN () for [], which is invalid SQL. Handle the empty case explicitly in this helper instead of relying on every caller to prefilter it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/shared/db/attendees/select.ts` around lines 291 - 306, Update the local
inList helper to handle empty ID arrays explicitly, preventing it from adding an
IN () clause while preserving normal filtering for non-empty arrays. Apply this
behavior to both attendeeIds and listingIds through the existing inList calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/shared/db/attendees/select.ts`:
- Around line 291-306: Update the local inList helper to handle empty ID arrays
explicitly, preventing it from adding an IN () clause while preserving normal
filtering for non-empty arrays. Apply this behavior to both attendeeIds and
listingIds through the existing inList calls.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6a1b19bc-7627-4766-9a81-88d6b3dac8b1

📥 Commits

Reviewing files that changed from the base of the PR and between 4d7d435 and 5e28d4e.

📒 Files selected for processing (9)
  • TODO.md
  • scripts/mutation/equivalent-mutants.txt
  • src/features/admin/attendees-edit.ts
  • src/features/admin/attendees-merge.ts
  • src/shared/db/attendees/queries.ts
  • src/shared/db/attendees/select.ts
  • src/shared/db/attendees/servicing.ts
  • src/shared/db/listings.ts
  • test/lib/db/attendees/select.test.ts

… invalid SQL

whereClauses' inList emitted `IN ()` for an empty attendeeIds/listingIds, which
is invalid SQL. No caller triggers it today (they pass one-element arrays or
prefilter empties), but the filter accepts any number[], so the builder should
stay total: an empty id set now emits `IN (NULL)` — always NULL, so no row
passes — matching the "empty filter matches nothing" behaviour the callers
already rely on. Covered by a direct builder test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/shared/db/attendees/select.ts`:
- Around line 293-295: Reword the comment explaining the empty ID set handling
to replace “stays total” with plain language describing that the query builder
remains valid and safe when callers provide an empty list. Keep the existing IN
(NULL) behavior and surrounding explanation unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 24558843-8f85-460c-af12-b8757e6f184c

📥 Commits

Reviewing files that changed from the base of the PR and between 5e28d4e and b896c8c.

📒 Files selected for processing (2)
  • src/shared/db/attendees/select.ts
  • test/lib/db/attendees/select.test.ts

Comment thread src/shared/db/attendees/select.ts Outdated
"stays total" was CS jargon (total function); say "still produces valid SQL"
instead, per the repo's plain-language rule for code comments. Comment-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G24MzgR46DN4ju1srifKaq
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 12, 2026
@stefan-burke
stefan-burke removed this pull request from the merge queue due to a manual request Jul 12, 2026
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 12, 2026
Merged via the queue into main with commit a81344a Jul 12, 2026
3 checks passed
@stefan-burke
stefan-burke deleted the claude/admin-homepage-queries-el4dr0 branch July 12, 2026 17:30
stefan-burke pushed a commit that referenced this pull request Jul 12, 2026
Main's #1785 (ledger/servicing split) and #1790 (attendee/listing reads) added
new code plus reorganised files my dedups touched. Restore/re-home the fixes so
the whole tree is clone-free at the tightened threshold:
- IdParam type in entity.ts, shared by site-pages idHandler and the relocated
  ledger postedTransferRoute.
- nameMapByIdsFor factory (config-object param, distinct from allNamesById) for
  the modifier/listing name lookups.
- decryptNameSlug helper for the news-summary and listing-catalog projections.
- Namespace-import groups in webhooks to break the checkin import-block match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DTrbBujCcBu8CUC56dy4tQ
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