Add Relation.belongsTo / Relation.has for FK edge ergonomics#89
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Relation helper API to reduce codegen boilerplate for FK edges while preserving existing query semantics (scope/context threading, where predicate, and cardinality), then updates codegen snapshots, examples, and tests accordingly.
Changes:
- Introduces
Relation.belongsTo/Relation.hashelpers and exportsRelationfrom the package entrypoint. - Updates table codegen to emit
Relation.*(...)calls (and auto-addRelationtotypegresimports in update mode), including relation direction metadata. - Regenerates example tables and adjusts codegen e2e/unit tests; adds focused unit tests for SQL parity, context threading, and card typing.
Reviewed changes
Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/tables/sqlite.test.ts | Updates sqlite codegen e2e snapshots to expect Relation.* helpers + imports. |
| src/tables/postgres.test.ts | Updates postgres codegen e2e snapshots/assertions for Relation.* helpers + imports. |
| src/tables/generate.ts | Emits Relation.belongsTo/Relation.has, adds relation direction, and ensures Relation is imported in update mode. |
| src/tables/generate.test.ts | Updates generator snapshots and adds coverage for default/non-default card opt emission + update-mode import behavior. |
| src/relation.ts | Adds the new relation helper implementation (context threading + FK predicate + cardinality). |
| src/relation.test.ts | Adds unit tests covering SQL parity, hydrate context propagation, card typing, and multi-column FK runtime rejection. |
| src/index.ts | Exports Relation from the root package surface. |
| examples/sqlite/src/tables/teams.ts | Regenerated example to use Relation.has. |
| examples/sqlite/src/tables/dogs.ts | Regenerated example to use Relation.belongsTo. |
| examples/basic/src/tables/toys.ts | Regenerated example to use Relation.belongsTo. |
| examples/basic/src/tables/teams.ts | Regenerated example to use Relation.has. |
| examples/basic/src/tables/microchips.ts | Regenerated example to use Relation.belongsTo with { card: "maybe" }. |
| examples/basic/src/tables/dogs.ts | Regenerated example relations to use Relation.* (including card overrides). |
| examples/basic/src/tables/collars.ts | Regenerated example to use Relation.belongsTo. |
| .gitignore | Ignores a local planning-notes file. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
ryanrasti
force-pushed
the
ryan_belongs_to_ergo
branch
from
July 18, 2026 02:42
77da6dc to
2d9ff50
Compare
ryanrasti
force-pushed
the
ryan_belongs_to_ergo
branch
from
July 18, 2026 22:48
f953dba to
7190e0d
Compare
ryanrasti
added a commit
that referenced
this pull request
Jul 18, 2026
Extend .live to sqlite (better-sqlite3, Durable Objects' SqlStorage) with
a minimal delta from the pg design: the Bus (reverse index, subscribe +
backfill, lifecycle) is dialect-shared; only the event source differs.
- Event source: pg keeps the shadow-events table + polling loop. sqlite
has NO shadow table and no polling — the runtime is a synchronous
single writer, so capture pushes events into the new `Bus.ingest()` in
the same tick as the mutation, entirely in memory. Cursors are integer
seqs embedded in the snapshot Cursor shape ({seq+1, seq+1, ∅}), so the
existing MVCC `visible()` test doubles as the seq comparison and the
subscribe-time backfill check precisely covers the register race.
- Capture (live/sqlite/capture.ts): opted-in tables (`transformer:
SqliteLiveCapture.makeTransformer()`) get json_object image columns
appended to RETURNING — insert→after, delete→before, update→after —
plus a pre-SELECT for update before-images (sqlite RETURNING can't see
OLD). Runs via a new optional `Driver.executeSync` in one synchronous
block with the mutation: no awaits between statements, so single-writer
atomicity replaces the transaction.
- Extractor portability: accept bare `Param` literals as anchors (sqlite
binds primitives as `?`, not pg's `CAST($n AS T)`); dialect-aware text
casts; decoy aliases so extractor CTEs never shadow their own table
(sqlite resolves a CTE name inside its body — "circular reference").
- live/canonical.ts: shared value-rendering contract; collapses integral
REALs so `'1.0'`-bound anchors match `'1'` stored INTEGERs (drivers
bind every JS number as REAL). A parity test hangs on divergence.
- Transport intentionally deferred: `.live()` stays a local AsyncIterable
on both dialects (already streams over exoeval). Cap'n Web prototype
learnings (by-ref callbacks vs map recorder, dup()/dispose lifecycle,
parked-await teardown) are recorded in ISSUES.md #13.
Tested end-to-end on better-sqlite3 (zero timers — dispatch is sync) and
via a fake SqlStorage through DoSqliteDriver; pg suite unchanged. Note:
examples/basic typecheck fails identically on main (8d8998b) — inherited
from #89, not this change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Collapse the codegen boilerplate
Target.scope(Current.contextOf(this)).where(...).cardinality(...)
into:
Relation.belongsTo(this, Target, { pk: this.fk }[, { card }])
Relation.has(this, Target, { fk: this.pk }[, { card }])
Helpers live as free functions under a Relation namespace (not on TableBase) so row/query autocomplete stays about columns and @expose methods. Substrate is unchanged: scope + contextOf + where + cardinality. Context threads through including undefined (same as today).
Card is an opts object for debugability and later attenuation params. Defaults: belongsTo → "one", has → "many"; codegen only emits { card } when non-default. Single-column FK maps only.
Codegen emits the helpers, ensures Relation is imported in update mode, and stamps direction on derived relations. Examples (basic, sqlite) regenerated. Unit tests cover SQL parity, hydrate context, and card typing.