Skip to content

feat(database): relational reads with defineRelations and findMany - #110

Merged
Makisuo merged 5 commits into
MapleTechLabs:mainfrom
artile:feat/relations
Nov 2, 2026
Merged

Makisuo merged 5 commits into
MapleTechLabs:mainfrom
artile:feat/relations

Conversation

@artile

@artile artile commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

effect-orm has no relational read API. Each nested read becomes manual joins and grouping in app code.

Closes #79.
Part of #52.

Changes

  • Db.defineRelations(tables, (r) => ...) in /database: r.one and r.many relations, declared over all tables in one callback, as Drizzle v2 defineRelations. A relation names its target by key, so tables can point at each other. The types check the target, the from and to columns, that both are string | number | bigint keys, and that a relation name is not a column name.
  • Db.findMany(schema.table, { where, orderBy, limit, offset, with, crossTenant }): every column of the table plus each relation in with, nested to any depth. Nested levels take where, orderBy and with. The row type follows the with tree: many is an array, one is a row or null.
  • One statement per level: WHERE <to> IN (<keys from the level above>), then grouping in memory. A level with no keys sends nothing. Each level is an ordinary builder query, so each level decodes through its own row codec (a nested timestamptz is a DateTime.Utc) and has its own tenantScope.
  • Tenant scope on every level: when both tables declare a tenantColumn and the parent rows carry one tenant, the nested query gets <tenant column> = <that tenant>. Otherwise the level is cross-tenant, and requireTenantScope refuses it. crossTenant: true declares every level.
  • design/relations.md: why one statement per level and not JSON aggregation, and the tenant rule.
  • docs/relations.md, a typechecked example, and reference / README rows. A minor changeset.

Left out

  • JSON aggregation (jsonb_agg, one statement). Each column type would need a JSON codec per dialect: int8 loses precision as a JSON number, timestamptz and bytea change format. It can come later as a second strategy behind the same API. See design/relations.md section 3.
  • columns per level, a limit per parent on a nested level, composite keys, and findFirst. Smallest slice first.
  • _count and some / every / none (Relation counts and filters: _count, some, every, none #80).
  • param.* in a nested where: findMany takes no params.
  • Chunking of long IN lists (Postgres has a 65,535 bound-value limit). This belongs with Bound-parameter limits: chunk IN lists and multi-row inserts #57.
  • SQLite, MySQL and D1: not on main. The SQL is a plain IN, so no dialect work is expected.
  • ClickHouse is not left out: relations need no foreign keys, and one live test runs the loader there.

Validation

  • bun run build: pass.
  • bun run typecheck: pass (includes src/database/relations.test-d.ts).
  • bunx vitest run --testTimeout=60000 --hookTimeout=60000 with live ClickHouse 26.8.2.7 (EFFECT_ORM_CLICKHOUSE_* set): 62 files, 1043 tests passed, 0 skipped.
  • node scripts/check-doc-citations.mjs, node scripts/check-exports-documented.mjs, node scripts/check-doc-examples.mjs: pass.
  • bun scripts/check-package.ts: pass.
  • Upstream CI for a fork PR waits for a maintainer to approve the run.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 49 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ebbbd0db-644d-434e-ae38-fbddeaa9c484
Commits

Reviewing files that changed from the base of the PR and between b57faf1 and f32db0a.

Files selected for processing: 12
  • .changeset/relational-reads.md
  • README.md
  • design/relations.md
  • docs/README.md
  • docs/reference.md
  • docs/relations.md
  • src/database.ts
  • src/database/relations.test-d.ts
  • src/database/relations.test.ts
  • src/database/relations.ts
  • src/sqlite/relations.test.ts
  • tests/relations.clickhouse.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

Makisuo and others added 2 commits November 2, 2026 16:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imit

A level with more keys than D1's 100 bindings failed to compile. Each level
now sends one IN statement per batch that fits maxParameters next to its
other bindings. Adds SQLite and D1 coverage and updates the docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Makisuo added a commit to artile/effect-orm that referenced this pull request Nov 2, 2026
Brings in origin/main and MapleTechLabs#110's IN-list batching. The count level and the
loaded level now share runBatched, so _count is split to the dialect's
bound-value limit too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Makisuo

Makisuo commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Brought up to date for merge (head f32db0a):

  • Merged origin/main (sqlite/d1 dialects, sqlite DDL/migrations, fuzz, Effect 4.0.6, afterCommit). It merged with no conflicts.
  • Fix, D1 bound-parameter limit (Bound-parameter limits: chunk IN lists and multi-row inserts #57): a level's IN (…) list put every key into one statement. On D1 (100 bindings) any level with more than about 100 parent keys failed with InvalidArguments. findMany now splits a level's keys into batches that fit dialect.params.maxParameters next to the level's other bindings, with one statement per batch. Each parent key goes into exactly one batch, so each parent's rows and their orderBy stay the same. Postgres (65,535) and SQLite (32,766) split only past their own limits. ClickHouse binds no params, so it never splits.
  • SQLite/D1 coverage: new src/sqlite/relations.test.ts. It loads nested many/one relations on node:sqlite with requireTenantScope and checks the tenant pin. It also loads a 250-key tree on a local D1 (Miniflare): 7 statements, each binding ≤ 100 values. Without the fix that test fails with "binds 251 values, over the 100 d1 allows".
  • Docs/design/changeset: the "chunking left out" and "SQLite/D1 not on main" notes are replaced with what the code now does.

Verified locally: bun run build, bun run typecheck, and bun run test against a live ClickHouse 26.8 (76 files / 1441 tests, plus the doc checks) all pass. tests/relations.clickhouse.test.ts ran live and passed. FUZZ_SCALE=0.3 vitest run tests/fuzz passed (269/269).

CI run 41472770236 failed, but not because of this PR. On 26.8, src/migrate/pg-enums-uniques.test.ts hit the 5000 ms timeout (a PGlite migrate test). On 26.2, the fuzz property "generated fixtures: union results agree (clickhouse)" failed on Amount % -4. Its seed passes locally on 26.8. This PR changes nothing under src/ch or tests/fuzz.

Left for the maintainer: the types do not require from and to to decode to the same JS type. A mismatch, such as an int4 key against an exact-bigint int8, fails loudly with InvalidLiteral when the IN list is encoded. It never gives silently wrong grouping.

Makisuo added a commit that referenced this pull request Nov 2, 2026
…te migrate tests 30s

ClickHouse 26.2 returns 3 for `-5 % -4` under UNION ALL (26.8: -1).
Record it as a known issue scoped to a negative constant divisor.

pg-enums-uniques starts a PGlite per test, which timed out at 5s on CI
runners busy with the fuzz suite (#110, #113, #114, #131).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Makisuo
Makisuo merged commit 555ee66 into MapleTechLabs:main Nov 2, 2026
1 of 3 checks passed
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.

Relational queries: nested with loading, decoded by Schema

2 participants