Skip to content

feat(core): declarative migrations, so content queries span a family's versions - #449

Merged
cuibonobo merged 2 commits into
mainfrom
feat/declarative-migrations
Oct 13, 2026
Merged

cuibonobo merged 2 commits into
mainfrom
feat/declarative-migrations

Conversation

@cuibonobo

Copy link
Copy Markdown
Member

Summary

Closes #448.

  • Declared steps replace the function form. migration(from, to, [rename, map, add, drop, derive]) builds migrate() from the steps and keeps them on the Migration. It checks them against both handles' schemas at construction: a step naming a field that isn't there, a carried field the target would refuse, or a required field nothing produces throws StackMigrationError. derive is the one opaque step (type changes, splits, computed values); its output is still validated at write time.
  • A collection's content filter or sort now spans older versions. Core composes each older version's steps into field origins and rewrites the query per version: rename and add translate; map translates filters (not sorts); derive narrows. Narrowed versions set skipped.older as feat(core): type a collection's content filters and sorts from its content type #447 did; newer versions stay out of reach.
  • Two wire shapes: filter.contentByType (a flat content arm per typeId) and sort.contentFieldByType ({ field } or a pre-reduced { value } per typeId). Each is gated by a capability that is a cap on keys; 0 declines it, and the collection falls back to feat(core): type a collection's content filters and sorts from its content type #447's narrowing. Local adapters declare 64.
  • Implemented in MemoryAdapter, sqlite-shared (both SQLite adapters), parseQueryBody(), normalizeCapabilities(); adapter-api carries both in the POST body unchanged.

Spec

  • docs/spec/data-model.md — new § Migration steps, § Content queries across versions, § Content by type; § Type migrations, § Type handles, § Sorting and pagination, § Sorting by a content field, § Capability-gated filters updated.
  • docs/spec/adapters.md § Adapter capabilities — the two caps and the local-required rule.
  • docs/spec/wire-format.md — discovery example, § Records (body-only shapes and their 400s), § Bounding query cost.
  • Small wording updates in apps.md and versioning.md ("migrations", not "migration functions").

Verification

All six from AGENTS.md pass locally: format:check, check:refs, lint, test (3,209 tests), build, typecheck.

New coverage: step construction checks and origin composition (type-handle, migration-steps tests); collection translation across rename/map/add/derive, a three-version chain, pagination under a translated sort, cap fallback and hand-built migrations, on Stack and ScopedStack; validation of both shapes; conformance-suite cases for both shapes (pass on MemoryAdapter and native SQLite); wire fixtures for both shapes and two bad_request cases.

Notes for reviewers

  • Sort key spelling: a separate sort.contentFieldByType mirroring the filter, rather than overloading contentField with a map — every reader of contentField treats it as a string.
  • Empty arms are allowed ({} matches every record at that typeId, no wider than typeId alone); an empty map is refused. An add constant that matches a filter needs this.
  • map keeps unlisted values, so map('status', { done: 'finished' }) has two v1 sources for 'finished'. The arm asks for 'done'; the first page probes for the other source and sets skipped.older on a match, rather than narrowing the issue's headline case. A second such key in one arm narrows.
  • Migration now requires steps; one built by hand has no origins and is narrowed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LREkawquacdfohXcxKRYWa


Generated by Claude Code

…s versions

A migration is now a list of declared steps — rename, map, add, drop,
derive — instead of a function. migration() checks them against both
handles' schemas when it runs, and keeps the field origins they imply.

A collection uses those origins to carry a content filter or sort into
each older version's shape, sent as filter.contentByType and
sort.contentFieldByType. A version that does not translate is narrowed
and reported in skipped.older, as before. Both shapes are capped by new
capabilities; local adapters declare 64, and a server declaring 0 gets
the handle-only query.

Spec, conformance fixtures, MemoryAdapter, sqlite-shared and the wire
parser move together.

Closes #448

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LREkawquacdfohXcxKRYWa
@changeset-bot

changeset-bot Bot commented Oct 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a40cee0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@haverstack/core Minor
@haverstack/wire-types Minor
@haverstack/conformance-fixtures Minor
@haverstack/adapter-conformance Minor
@haverstack/record-adapter-sqlite Minor
@haverstack/record-adapter-do-sqlite Minor
@haverstack/adapter-api Patch
@haverstack/adapter-local Patch
@haverstack/blob-adapter-disk Patch
@haverstack/blob-adapter-s3 Patch
@haverstack/commons Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…sions

Review on #449:

- A field the handle declares and no step produces is read under its own
  name at an older version, since migrate() carries every field no step
  names; only a name a step removed is "no value". An older version that
  gained the field in place was otherwise silently dropped from a filter
  and sorted last.
- A content query's cursor now carries the versions its first page chose,
  so a version defined mid-pagination cannot change the sort map a cursor
  resumes, and a later page no longer lists types before querying.
- Shorter comments, one withoutContent() helper, validate.ts's
  isPlainObject() reused, and no "legacy" field in tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LREkawquacdfohXcxKRYWa
@cuibonobo
cuibonobo merged commit 8553d95 into main Oct 13, 2026
9 checks passed
@cuibonobo
cuibonobo deleted the feat/declarative-migrations branch October 13, 2026 14:48
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.

Declarative migrations, so content filters and sorts can span a family's versions

2 participants