Closes #519
/claim #519
This PR implements a pure, algebraic schema migration system for ZIO Blocks. The architecture shifts
migration validation from runtime to compile time, ensuring mathematically sound shape equivalence
between schema versions.
Pure data core. Migrations are modelled as a MigrationAction ADT — no closures, opaque functions,
or reflection. Every migration is fully serialisable.
Selector macro API. Custom macros for Scala 2.13 (scala-reflect) and Scala 3 (scala.quoted)
extract S => A selector lambdas into DynamicOptic paths at compile time. The developer-facing API is
identical across both versions:
MigrationBuilder[PersonV1, PersonV2]
.renameField(_.name, _.fullName)
.build
Two-layer design.
DynamicMigration — untyped, serialisable core operating on DynamicValueMigration[A, B] — typed façade with Schema-driven encode/decodeThe .build macro performs a symbolic dry-run of the builder AST against structural Refinement types,
proving shape equivalence at compile time. A missing or misaligned field is a compiler error, not a
runtime failure. Structural types (e.g. type PersonV0 = { def firstName: String }) are natively
supported for past schema versions without requiring concrete case classes.
Cross-version AST stability is maintained across Scala 3.3.x and 3.7.x — the SelectorMacro intercepts
both Ident and Select patterns arising from reflectiveSelectableFromLangReflectiveCalls desugaring.
End-to-end proof is in jvm/src/test/scala-3/.../StructuralTypeMigrationSpec.scala.
MigrationLawsSpec verifies the following invariants via property-based testing:
empty.apply(v) == Right(v) for any Record or Variant(m1 ++ m2) ++ m3 and m1 ++ (m2 ++ m3) produce identical resultsm.reverse.reverse.actions.length == m.actions.length; round-trips recover the44 tests pass across Scala 2.13, 3.3, and 3.7 on both JVM and JS targets.
https://github.com/user-attachments/assets/01f3e8e1-731a-4762-b7eb-46dc82d20a13
Dakoda
@DakodaStemen
marianaguzmanguerrero16-dev
@marianaguzmanguerrero16-dev
ZIO
@ZIO