Skip to content

Let a read say what it wants instead of spelling out SQL - #1982

Merged
stefan-burke merged 9 commits into
mainfrom
claude/attendee-pattern-collections-pfvtod
Jul 29, 2026
Merged

stefan-burke merged 9 commits into
mainfrom
claude/attendee-pattern-collections-pfvtod

Conversation

@stefan-burke

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

Copy link
Copy Markdown
Member

What changed

We had three good pieces that did not fit together.

A table already knew its columns and how to read them. A set of filters already
knew which rows to keep and which values fill them. But every read still wrote
the join between those two by hand — the SELECT, the FROM, the WHERE, the
ORDER BY, the LIMIT. That handwriting is where the last of the raw SQL lived,
and where a filter could drift away from its values.

A column list now knows how to fetch itself. You tell it which rows you want,
in what order, and how many; it writes the query, because it already knows its
own table and its own columns.

What that tidied up

Thirteen reads across images, listings, the catalog, API keys, site pages,
groups and notes now say what they want instead of spelling out a query.

Two doors closed. Site pages and notes each took a piece of SQL as text
from their callers — the same door this work has been shutting elsewhere. Both
now take described filters.

A fourth way of asking, days old. Notes had grown its own way of describing
which rows to fetch. It now uses the same one as everything else. A pleasant
side effect: asking for the notes of no records used to be refused outright,
and now simply costs nothing.

One check instead of three. Every reader skips the database when what it
was asked for cannot match any row. That check had been written out three
separate times; it is now one shared piece that all of them go through.

What is deliberately left alone

Reads that join a second table or carry a subquery still write their own SQL —
images by use, attributes by listing, news posts, the group candidate list, and
the two big listing and attendee readers. They keep using the same column lists
and the same filters underneath, so nothing is duplicated; they simply say
more than a single-table read can.

The aim is one way to say which rows, one way to say which columns, and no
handwriting in between — not a layer that hides what a complicated query does.

Tests

  • New tests for the declared read: filtering, ordering, capping, the
    single-row form, and that a read which cannot match anything asks the
    database nothing.
  • The notes tests now describe the shared filters rather than the old shape.
  • One pinned query string gained a row cap, because a single-row read now asks
    for a single row. The columns it selects — what that test is about — are
    unchanged.
  • A method with no callers left was removed rather than kept for symmetry.

Full checks pass, including complete test coverage.


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added standardized column-based data reads for more consistent handling of listings, images, news posts, API keys, and site pages.
    • Added query helpers that safely handle filters matching no records without unnecessary database requests.
  • Bug Fixes
    • Improved consistency when loading selected fields and applying data transformations.
    • Updated public image data types across ticket, listing, news, and reservation views.
  • Tests
    • Expanded coverage for filtered, ordered, limited, and no-result data reads.

claude added 2 commits July 28, 2026 23:05
We had three good pieces that did not fit together. A table knew its columns
and how to read them. A set of filters knew which rows to keep and what values
to fill them with. But every read still wrote the join between them by hand:
the SELECT, the FROM, the WHERE, the ORDER BY, the LIMIT. That handwriting is
where the last of the raw SQL lived.

A column list now knows how to fetch itself. Ask it for the rows you want and
it writes the query, because it already knows its own table and columns.

Thirteen reads across images, listings, the catalog, API keys, site pages,
groups and notes now declare what they want. Two of them had taken a piece of
SQL as text from their callers; those doors are closed.

Notes had grown a fourth way of describing which rows to fetch, days old. It
now uses the same one as everything else, which also means asking for notes
about no records costs nothing rather than being refused.

Every reader skips the database when what it asked for cannot match any row,
through one shared piece rather than three copies of the same check.

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

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0bcbce07-4ba2-4b70-a1d7-d5f8c3d3591d

📥 Commits

Reviewing files that changed from the base of the PR and between ef36044 and f074b89.

📒 Files selected for processing (4)
  • src/shared/db/chosen-columns.ts
  • test/integration/api-keys.test.ts
  • test/shared/db/chosen-columns/columns.test.ts
  • test/shared/db/chosen-columns/select.test.ts
📝 Walkthrough

Walkthrough

The PR replaces table projections with chosen-column readers, adds shared typed query helpers, migrates database read paths, updates listing and image row types, and renames ledger projection helpers.

Changes

Database read migration

Layer / File(s) Summary
Chosen-column and query foundations
src/shared/db/chosen-columns.ts, src/shared/db/table.ts, src/shared/db/where-clauses.ts, test/shared/db/chosen-columns/*
Adds chooseColumns, queryTail, and rowsUnlessNoneMatch, removes table projections, and tests selected-column reads and no-match behavior.
Database module read adapters
src/shared/db/api-keys.ts, src/shared/db/attributes.ts, src/shared/db/groups/*, src/shared/db/listings/catalog.ts, src/shared/db/news-posts.ts, src/shared/db/site-pages.ts, test/integration/api-keys.test.ts
Migrates narrow reads to chosen columns and typed predicates across API keys, attributes, groups, catalogs, news posts, and site pages.
Listing and image read paths
src/shared/db/listings/*, src/shared/db/images.ts
Composes listing stored columns, updates listing row types, and replaces image projection reads with chosen-column decoding.
Typed where and query-tail migration
src/shared/accounting/queries.ts, src/shared/db/activityLog.ts, src/shared/db/notes/*, test/shared/db/notes/target.test.ts, test/shared/db/where-clauses.test.ts
Refactors transfer, activity-log, and note queries to shared where-clause construction and centralized empty-match handling.
Public image and ledger contracts
src/shared/types.ts, src/features/public/types.ts, src/ui/templates/public/*, src/shared/ledger/project.ts, test/shared/ledger/project.test.ts
Replaces ItemImageProjection with ItemImageColumns and renames listing cost and profit helpers with updated tests.

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

Possibly related PRs

🚥 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 captures the shift from hand-written SQL to declarative reads driven by selected columns and filters.
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/attendee-pattern-collections-pfvtod
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/attendee-pattern-collections-pfvtod

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

A pass over the new code against AGENTS.md, before anyone else reads it:

The column-list code moved out of the table file into its own. That file had
grown to 955 lines, forty short of the size we fail a build at, and reading a
chosen set of columns is its own idea with its own name. Its tests moved with
it.

The helper that skips a pointless read is now named for what it checks — that
no row can match — rather than sounding like it checks whether the rows are
empty. The check behind it is no longer offered to callers, since nothing
outside used it; two tests that reached for it now watch what actually happens
instead.

The ledger's newest-first list still built its own ending — the filter, the
order and the cap — as did the activity log and the column lists. All three
now share one piece, and say what they return by its proper name rather than
spelling the shape out again.

Three long comments cut back to the one thing each was there to say.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 312cf445cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/shared/db/projection.ts Outdated
* joins or carries a subquery writes its own SQL and runs it through
* {@link TableProjection.queryAll}.
*/
export type ProjectionSelect = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rename the new projection API in plain domain language

Rename the newly exported ProjectionSelect surface, and the associated new module, around the plain concept already stated here—reading chosen table columns. projection is explicitly listed as CS jargon to avoid when a plain phrase works, and exposing it in the public type and import path spreads that vocabulary to every future database reader.

AGENTS.md reference: AGENTS.md:L69-L70

Useful? React with 👍 / 👎.

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.

You're right, and this is a fair hit — I introduced projection into a module name and an import path during a pass whose whole purpose was conforming to AGENTS.md, where the word is listed by name.

Fixed in 2faaed9, and I went wider than the new surface. Renaming only ProjectionSelect would have left a plainly-named file exporting jargon, so the whole vocabulary moved:

Was Now
db/projection.ts db/chosen-columns.ts
defineTableProjection chooseColumns
TableProjection ChosenColumns
ProjectionSelect ReadRequest
StoredTableProjectionRow StoredRowOf
ItemImageProjection ItemImageColumns
xxxProjection locals xxxColumns

The call site reads as the plain sentence now: chooseColumns(rawListingsTable, ["id", "name", "active"]), then .select({ where, order }).

The word was in comments and two error messages as well, so those changed too — "Cannot select projected column X" became "Cannot select X from Y: it is not one of its columns", and the two tests pinning those messages were updated.

66 references across 12 files; full precommit green.


Generated by Claude Code

Comment on lines +2 to +4
* Tests for a projection's declared read (`select` / `selectOne` in
* `src/shared/db/table.ts`), which composes the projection's columns and table
* with the shared filter clauses so an ordinary single-table read needs no SQL.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Point the test comment at the moved module

Update this comment to reference src/shared/db/projection.ts: this commit moved select and selectOne out of table.ts, so the current pointer now sends maintainers to a file that no longer contains the code under test. The repository requires comments to describe the current code rather than preserve stale information.

AGENTS.md reference: AGENTS.md:L71-L71

Useful? React with 👍 / 👎.

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.

Fixed in 2faaed9 — the comment pointed at table.ts, which no longer holds the code under test.

The rename in the sibling thread moved the module again, so the comment now names src/shared/db/chosen-columns.ts, and the test directory moved to test/shared/db/chosen-columns/ to mirror it. That also keeps the mutation gate's rule that a source file has a direct test at the matching path.


Generated by Claude Code

claude added 2 commits July 29, 2026 01:02
Our own guide lists "projection" as a word to avoid when a plain phrase says
the same thing, and this branch had just put it in a file name and in the
import line of every reader that picks its columns.

The idea is choosing a set of a table's columns, so that is what it is called
now: you choose columns, and what you get back is the chosen columns. The
error messages and comments say it in plain words too, and the tests sit
beside the file they cover.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2faaed9bc5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/shared/ledger/project.ts Outdated

/** Positive cost total for one listing. Cost legs source `cost:<listingId>`. */
export const costProjection =
export const costColumns =

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Rename costColumns for the value it computes

The new costColumns export does not read or return columns: it returns a curried calculation of one listing's numeric cost from transfers, and profitColumns repeats the same mismatch. These public names now suggest database column selection instead of ledger calculations, so use plain domain names such as costOfListing and profitOfListing.

AGENTS.md reference: AGENTS.md:L69-L70

Useful? React with 👍 / 👎.

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.

Good catch, and it exposed a bigger problem than the two names you flagged.

I did the projectionchosen columns rename with a tree-wide regex (\b(\w+)Projection\b), which caught every identifier ending in that word regardless of what it meant. Auditing all 22 renames, three were wrong:

  • costProjection / profitProjection — ledger sums, exactly as you say. Now costOfListing / profitOfListing, which reads better than the old name too.
  • CatalogProjection, StoredValueProjection, fieldSupportsProjection in catalog-fields/definition.ts — these describe how a catalog field is stored, not columns. My rename produced fieldSupportsColumns, which means nothing. Reverted to their original names; renaming those properly isn't this change's business.

The other 19 are genuine column selections and stay renamed. I also made listingDayPriceColumns singular, since it builds one column.

Fixed in ef36044. Current main is merged in as well — the branch had gone behind. Full precommit green.


Generated by Claude Code

Renaming "projection" to "chosen columns" was done with a search and replace
across the whole tree, and it caught names that had nothing to do with reading
columns.

Two of them work out money from the ledger — one listing's cost and its profit
— so they are now named for the values they return. Three others describe how
a catalog field is stored, and are back to the names they had, since renaming
those is not this change's business.

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

Copy link
Copy Markdown
Member Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

@stefan-burke I’ll review the changes in #1982.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@stefan-burke
stefan-burke added this pull request to the merge queue Jul 29, 2026
@stefan-burke
stefan-burke removed this pull request from the merge queue due to a manual request Jul 29, 2026
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@stefan-burke

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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/chosen-columns.ts`:
- Around line 129-135: Update statementFor to include table.primaryKey in the
internal SELECT whenever the requested columns omit it, so row-ID-dependent
transforms receive the stored key while the public read result still returns
only the requested columns. Preserve the existing selection when the primary key
is already requested, and add a regression test covering select() without id
using the local tag transform.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f73573a4-62af-4add-a3cf-9e9d18b4e033

📥 Commits

Reviewing files that changed from the base of the PR and between 62addbb and ef36044.

📒 Files selected for processing (33)
  • src/features/public/types.ts
  • src/shared/accounting/queries.ts
  • src/shared/db/activityLog.ts
  • src/shared/db/api-keys.ts
  • src/shared/db/attributes.ts
  • src/shared/db/chosen-columns.ts
  • src/shared/db/groups.ts
  • src/shared/db/groups/candidates.ts
  • src/shared/db/images.ts
  • src/shared/db/listings/attendees.ts
  • src/shared/db/listings/catalog.ts
  • src/shared/db/listings/records.ts
  • src/shared/db/listings/select.ts
  • src/shared/db/listings/table.ts
  • src/shared/db/news-posts.ts
  • src/shared/db/notes/queries.ts
  • src/shared/db/notes/target.ts
  • src/shared/db/site-pages.ts
  • src/shared/db/table.ts
  • src/shared/db/where-clauses.ts
  • src/shared/ledger/project.ts
  • src/shared/types.ts
  • src/ui/templates/public/reservations/form.tsx
  • src/ui/templates/public/reservations/og-tags.ts
  • src/ui/templates/public/reservations/ticket-page.tsx
  • src/ui/templates/public/reservations/types.ts
  • src/ui/templates/public/shared.tsx
  • test/integration/api-keys.test.ts
  • test/shared/db/chosen-columns/columns.test.ts
  • test/shared/db/chosen-columns/select.test.ts
  • test/shared/db/notes/target.test.ts
  • test/shared/db/where-clauses.test.ts
  • test/shared/ledger/project.test.ts
💤 Files with no reviewable changes (1)
  • src/shared/db/table.ts

Comment thread src/shared/db/chosen-columns.ts
Some columns are stored sealed, and if one will not open the reader records
which row it came from so an operator can find it. It works that out from the
row's own key — but a read that had not asked for the key never received it,
so the record would have said "image undefined" and named nothing.

Reads now fetch the key whether or not it was asked for, and drop it again
before handing the row back, so the caller still gets exactly the columns it
chose.

No read does this today: the two image reads happen to ask for the key
already. It was waiting for the next one that did not.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab550d6087

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

import { describeWithEnv } from "#test-utils/db.ts";
import { createTestListing } from "#test-utils/db-helpers/listings.ts";

describeWithEnv("db > table > declared projection reads", { db: true }, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove projection jargon from the renamed tests

Fresh evidence after the earlier API rename is that this new chosen-columns test still labels the suite declared projection reads; test/shared/db/chosen-columns/columns.test.ts likewise retains ProjectionRow, projectionTable, and projection_rows. This leaves the explicitly forbidden term throughout the tests for the renamed mechanism, so rename these fixtures and descriptions consistently around “chosen columns.”

AGENTS.md reference: AGENTS.md:L69-L70

Useful? React with 👍 / 👎.

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.

Fair — the rename stopped at the production surface and left the word standing in the tests for the very mechanism it renamed. Fixed in ccff42b:

Was Now
ProjectionRow SampleRow
projectionTable sampleTable
"projection_rows" "sample_rows"
"db > table > declared projection reads" "db > chosen columns > declared reads"

The suite title also said db > table >, which was stale twice over — the code moved out of table.ts two commits ago — so it now names the module it actually tests.

One word stayed on purpose: "runs projected column transforms" names col.projected, a table.ts column builder this branch doesn't touch. Renaming a production builder is a separate change, and the test title should keep matching the thing it calls.

Full precommit green.


Generated by Claude Code

claude added 2 commits July 29, 2026 10:59
The tests for the renamed mechanism still called their fixture table a
projection, which is the word the rename was meant to remove.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UASQuuj6grCBBrHHQeqMCF
@stefan-burke
stefan-burke added this pull request to the merge queue Jul 29, 2026
Merged via the queue into main with commit 4072ed0 Jul 29, 2026
3 checks passed
@stefan-burke
stefan-burke deleted the claude/attendee-pattern-collections-pfvtod branch July 29, 2026 13:26
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