feat(effect-app,infra): fail store document decode as a typed SchemaError - #898
Merged
Merged
Conversation
@effect-app/cli
effect-app
@effect-app/eslint-codegen-model
@effect-app/eslint-shared-config
@effect-app/infra
@effect-app/vue
@effect-app/vue-components
commit: |
…emaError `makeStoredDecode` now decodes with `S.decodeUnknownResult` and returns a `Result`, so a document `jitM` does not repair is a typed `S.SchemaError` instead of a defect thrown out of `decodeSync`. Nothing became lenient: the same documents still fail, only inspectably. The decode stays synchronous - one loop per operation, fail-fast, lifted once with `Effect.fromResult` - so no Effect is allocated per document. Cosmos, SQL, SQL/Pg, Memory and Disk lift it into their channel, and `Store`'s decoding members declare `DatabaseError | SchemaError`. The repository internals let that failure propagate and die on it where the public members are produced, so `Repository.find`/`all`/`saveAndPublish`/ `removeById` keep their declared error types. `validateSample` now records a store-boundary failure as a `ValidationError` for that document and keeps sampling, instead of losing the whole run to a defect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dary The "fails loudly" case now asserts a typed `S.SchemaError` from the store (via `Effect.result`, which a defect would escape), plus a new test that the same document still dies on a public repository read, and one that `validateSample` reports it as a `ValidationError` while the good document still validates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
patroza
force-pushed
the
feat/store-decode-schema-error
branch
from
September 16, 2026 08:11
11cae16 to
764e79d
Compare
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.
Follow-up to #896 (merged). Phase 1 of making store decode failures inspectable: the store's document decode now fails as a typed
S.SchemaErrorinstead of throwing a defect, the failure travels up through the repository internals, and the publicRepositorysignatures are unchanged — they die at one explicit boundary.Why
makeJsonDocumentCodec.decodeisS.decodeSync, so a documentjitMdoes not repair throws inside adaptermapcallbacks. Two consequences:validateSample— whose whole job is reporting bad documents — dies instead of recording one. Diagnosing a real case (a container holding only libs' own seed marker) meant reproducing it by hand to learn which key was missing.Nothing here becomes lenient. An unrepaired document still fails; it fails typed.
Shape
Result, notExit:S.decodeUnknownResultgives a two-case tagged value with noCauseallocation,Effect.fromResultlifts it in one step, andvalidateSamplealready inspects decode outcomes asResult. HelpersdecodeStoredMany(sync fail-fast loop, oneResultper batch) anddecodeStoredOptionsit beside it.Deliberately synchronous. The decode runs once per document over whole result sets, so there is no
Effectper row: each read operation runs one sync loop that fails fast and lifts once. AllannotateDb/span/timeSchemainstrumentation is preserved — the decode moved fromEffect.mapintoEffect.flatMapinside the same annotated pipelines.Channels
Storemembers that decode now declareDatabaseError | SchemaError, each with a one-line reason:all,find,FilterFunc/filter,queryRaw, and the write members that decode the written document on the way back (set,batchSet,bulkSet— Memory mapsdecodeDocthere).batchRemove/seedNamespaceunchanged.Model/Repository/service.tsis not in this diff — no public signature changed. The single boundary isinternal/internal.tswhere the public members are produced:find,all,saveAndPublish,removeByIdeachEffect.catchTag("SchemaError", (e) => Effect.die(e)). DeliberatelycatchTagand notorDie, which would have eatenDatabaseErroras well.queryRaw,queryandmappedalready declaredS.SchemaErrorand simply propagate.The payoff
validateSamplenow distinguishes the two failure sources: a store-boundary failure arrives instore.find's error channel (captured withEffect.result, narrowed withS.isSchemaError, aDatabaseErrorre-failed) and is recorded asValidationError{ id, rawData: undefined, error }— undefined because the store failed before it could hand back a document; its own decode failure keepsrawData. Either way the loop continues, so one bad document no longer costs the whole run.Verification
pnpm check(tsgo): clean; no new casts.packages/infra: 284 passed / 26 skipped.packages/effect-app: 185 passed. Lint clean.SchemaErrorviaEffect.result(a defect would escape it); a new test asserts the same document still dies throughrepo.all(Cause.hasDiestrue,hasFailsfalse), proving the public surface is unchanged; another assertsvalidateSamplereturnsvalid: 1with oneValidationErrornaming the bad document and no death.Two judgement calls for your review
makeStoredDecodenow takes the store'sschemarather than theJsonDocumentCodec, because building a non-throwing decoder needsS.decodeUnknownResult(the codec only exposesdecodeSync).makeJsonDocumentCodecis unchanged and still used by the write path; both share one internaljsonCodechelper.Effect.orDie: that read seeds the in-memory store andmakeDiskStoreIntis alreadyorDie'd by both callers, so a typed failure there cannot reach anyStoremember.Also: SQL/Pg
parseRowlost itsdecodeparameter and is now purely "row → raw JSON document", with the adapters composingdecodeStored(parseRow(...)). That keeps all 9 existingparseRowtests compiling untouched.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.