fix: derive persistability from source presence, not object subtype - #249
Merged
Conversation
…, not subtype Full review (Fable): both halves of the fail-open — migrate-ts Pass 1 phantom CREATE TABLE for any sourceless object, and codegen-ts queries/routes/api-model emitting DB-bound artifacts (broken imports) for sourceless objects incl. plain object.value. Principle: an object participates in the DB iff it declares/inherits a source.* — the loader's already-published contract (validate-source-roles: "zero sources ⇒ not persisted"). npm-only PATCH. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
SDD plan: (1) migrate-ts Pass 1 collapse to `if (!hasWritableSource) continue;` + tests incl. custom-subtype provider; (2) codegen-ts hasAnyRdbSource + queries/ routes/hono/api-model filters + no-churn probe; (3) reference/scaffold templates; (4) integration/golden/typecheck gates; (5) CHANGELOG. Byte-identical for well-formed models. npm-only PATCH (migrate-ts + codegen-ts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…ce presence, not subtype buildExpectedSchema Pass 1 decided "is this a table" via a hardcoded `subType === "value"` compare, so any object with no source.* (a custom subtype, or a plain sourceless entity) fell through and got a phantom CREATE TABLE with a fabricated physical name (entering the FK-target maps under that fake name). Replace the subtype compare + the read-only-only projection skip with a single rule: skip an object unless it declares or inherits >=1 WRITABLE source (ADR-0039 resolving children), matching the loader's own already-published contract that zero sources means "not persisted" (validate-source-roles.ts). Subsumes both prior skips exactly (value purity bans sources on the resolving view; read-only-only implies !hasWritableSource) while closing the fail-open for sourceless objects of any subtype. Also adds a bare `source.rdb` to ~18 pre-existing migrate-ts test fixtures/inline models that declared no source at all — they only produced a table under the old subtype-based fallback. A source with no @table/name resolves its physical name via the same entity-name fallback the old sourceless path used, so table names are unchanged; this just makes the models well-formed under the corrected rule instead of leaving the suite exercising an empty schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…ce, not subtype R2 (design spec §3): an object participates in DB-bound codegen iff it declares/inherits a source.rdb child, never by subtype. New hasAnyRdbSource (source-detect.ts) — sibling of the entity-file tier's hasWritableRdbSource, but ANY source.rdb kind (writable or read-only), so a read-only projection stays queryable. queries-file.ts, routes-file.ts, routes-file-hono.ts, and api-model.ts's isQueryable now gate on it instead of subType !== OBJECT_SUBTYPE_VALUE (queries) or no gate at all (routes/hono — the latter was a pre-existing fail-open: a plain object.value got a broken routes file importing exports that were never generated). Adapted three pre-existing tests/fixtures that only worked because of this bug (stashed-baseline-verified, per Task 1's carry-forward): - codegen-ts-tanstack multi-grid-entity.json: Program had no source.rdb at all; added a bare one (byte-identical physical naming) so the full-pipeline integration test's queries/routes files keep existing. - codegen-ts generators/queries-file.test.ts: entityWithPk() helper built entities with no source; added a bare source.rdb so "keeps every object.entity" fixtures stay queryable, and added a new case pinning the actual new behavior (a sourceless object.entity is excluded, not just object.value). - codegen-ts reference-byte-identical.test.ts: carved out one precisely identified, tracked, temporary divergence (Triple.routes.ts in cross-package-vo.json) — the reference (scaffold-and-own) generators get the same fix in the very next commit (#248 Task 3); until then the built-in/reference composers diverge by exactly the DB-bound artifact this fix correctly stops emitting for a sourceless value object. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…rce presence Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…urce-derived persistability Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
…npm-only patch) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HLoJkFSyoticveo5ehMUAr
This was referenced Aug 2, 2026
Closed
Closed
Closed
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.
Intent
Issue #248 (npm-only PATCH, packages migrate-ts + codegen-ts): persistability derives from a declared/inherited source.* child, NEVER from object subtype. (1) migrate-ts expected-schema Pass 1: replaced a hardcoded subType==="value" compare with a "skip unless a writable source is declared/inherited" gate, so a sourceless object no longer gets a phantom CREATE TABLE or a fabricated FK-target name. (2) codegen-ts queries/routes/routes-hono/api-model plus the src/reference/* scaffold copies: gate DB-artifact emission on a new hasAnyRdbSource helper (mirrors the already-correct Drizzle table tier's hasWritableRdbSource), fixing broken generated files for sourceless objects INCLUDING a pre-existing object.value broken-routes fail-open. Plus byte-identical bare-source fixture adaptations across consuming packages (cli/integration-tests) and a CHANGELOG entry. This ALIGNS both tiers to the loader's already-published contract (validate-source-roles: zero sources means not persisted) -- it is a BUGFIX, not a behavior-contract change; meta gen / meta migrate output is byte-identical for well-formed models (the norm, where every table-owning object declares or inherits a source). A whole-branch senior review already returned Ready-to-merge with no Critical/Important findings. Closes #248.
DELIBERATELY OUT OF SCOPE -- design-approved deferrals (design spec section 9), do NOT flag these as missing: a cross-port LOADER error for an enforced identity.reference to a non-persisted target (9a); a cross-port codegen audit of the same fail-open in the Java/Python/Kotlin/C# generators (9b -- those ports emit no DDL, lower stakes); client hook parity in the react/tanstack client packages (9c); a migrate/verify --entities scope filter (9d); routes-file-hono TPH gating (9e, a pre-existing gap, no regression here).
KNOWN-DEFERRED cosmetic MINORS (the final review triaged these defer-acceptable -- do NOT block merge on them): a double-wrapped expect(()=>{...}).not.toThrow() around an assertion in expected-schema-persistability.test.ts test 2; stale mirror-comment line references at api-model.ts around line 347; a queries-file.test.ts test name that reads as subtype-based though the filter is now source-based; the api-model isQueryable sourceless-exclusion not pinned by a dedicated test; soft ("roughly 150") blast-radius numbers in the CHANGELOG. KNOWN RESIDUAL: the full packages/integration-tests suite (16 files beyond the 2 view-lifecycle files that were run green) was not run locally -- CI's slow lane covers it, and the reviewer scanned all 16 and confirmed none relies on the phantom-table behavior.
What Changed
subType === "value"check, so sourceless objects no longer emit phantomCREATE TABLEstatements or fabricated FK-target names.src/reference/*scaffold copies) on a newhasAnyRdbSourcehelper, so sourceless objects no longer emit broken DB artifacts — closing a pre-existingobject.valuebroken-routes fail-open.Closes #248.
Risk Assessment
✅ Low: A focused, well-tested npm-only bugfix that aligns migrate-ts and codegen-ts to an already-enforced loader invariant (zero sources ⇒ not persisted): byte-identical for well-formed models, all load-bearing assumptions independently verified (value-source ban is loader-enforced on the resolving view; bare-source fixture naming falls through to the same pluralized-snake-case fallback), and every behavioral change is pinned by new tests — the only residual items are the author's explicitly-deferred cosmetic minors and cross-port follow-ups.
Testing
Exercised the fix at three levels, all green: the two new pinning unit tests, the directly-affected regression unit suites (migrate-ts expected-schema family + codegen-ts queries/byte-identical, gating the byte-identical-for-well-formed-models claim), and the real-DB view-lifecycle integration tests (Testcontainers Postgres + SQLite). Then demonstrated the end-user surface by driving the actual
metaCLI on a sourceless-object fixture with an Order→sourceless-Ghost reference:meta genemits no DB-bound artifacts for sourceless/value objects, andmeta migrateemits a single CREATE TABLE with no phantom table and no fabricated FK target. No failures or setup issues.Evidence: meta gen transcript — sourceless-object fixture
Fixture: Order (source.rdb orders, identity.reference→Ghost), Ghost (entity, NO source), Money (object.value, NO source). $ meta gen summary: 6 written gen-out/Order.ts, gen-out/Ghost.ts, gen-out/Money.ts, gen-out/Order.queries.ts, gen-out/Order.routes.ts, gen-out/Order.routes.hono.ts DB-bound artifacts for sourceless/value objects (MUST be absent): absent(correct): Ghost.queries.ts, Ghost.routes.ts, Ghost.routes.hono.ts, Money.queries.ts, Money.routes.ts, Money.routes.hono.ts Ghost.ts is a clean shape-only interface (no Drizzle table/allowlist import); Order.routes.ts imports the realorderstable from ./Order.js. Before #248, routes-file.ts emitted Ghost.routes.ts importing a never-generatedghoststable/allowlist (TS2305/TS2724 breakage).Evidence: meta migrate transcript — sourceless-object fixture
$ meta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --dry-run -- UP -- CREATE TABLE "orders" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "reference" TEXT, "ghost_id" INTEGER ); changes: 1 create-table Result: exactly ONE CREATE TABLE (orders, the sourced entity). No phantom CREATE TABLE "ghosts"/"moneys" for the sourceless/value objects, and ghost_id is a plain INTEGER column with NO fabricatedREFERENCES ghosts(...)FK — before #248 the sourceless Ghost got a phantom table + fabricated FK-target name.Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
bun test packages/migrate-ts/test/expected-schema-persistability.test.ts (4 pass — the migrate Pass-1 gate pin: sourceless entity → no table, no fabricated FK target, custom subtype → no table, inherited writable source via extends → persists)bun test packages/codegen-ts/test/sourceless-objects.test.ts (4 pass — engine + reference generators gate queries/routes/hono on hasAnyRdbSource; read-only projection still queryable; Order.* byte-identical whether or not sourceless objects co-load)bun test packages/migrate-ts/test/expected-schema.test.ts expected-schema-tph.test.ts expected-schema-fk-constraint-name.test.ts expected-schema-schema-aware.test.ts expected-schema-field-object-storage.test.ts unit/expected-schema*.test.ts (56 pass — regression cover for table/view partition)bun test packages/codegen-ts/test/generators/queries-file.test.ts reference-byte-identical.test.ts sourceless-objects.test.ts (14 pass — byte-identical-for-well-formed-models gate)bun test packages/integration-tests/test/view-lifecycle-pg.test.ts view-lifecycle-sqlite.test.ts (18 pass — real Postgres+SQLite via Testcontainers; the migrate table-vs-view partition applies round-trip)meta gen on a fixture (Order sourced + identity.reference→Ghost; Ghost sourceless entity; Money object.value) — 6 files written, all Ghost.*/Money.* queries/routes/hono absentmeta migrate --from-db --db file:dev.sqlite --dialect sqlite --slug init --dry-run on the same fixture — exactly 1 CREATE TABLE orders, no phantom ghosts/moneys table, ghost_id plain column with no fabricated REFERENCES ghosts(...) FK✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.