Skip to content

feat(schema): implement Schema Migration System (#519)#968

Closed
yuvrajangadsingh wants to merge 33 commits intozio:mainfrom
yuvrajangadsingh:feat/schema-migration-519-v2
Closed

feat(schema): implement Schema Migration System (#519)#968
yuvrajangadsingh wants to merge 33 commits intozio:mainfrom
yuvrajangadsingh:feat/schema-migration-519-v2

Conversation

@yuvrajangadsingh
Copy link

@yuvrajangadsingh yuvrajangadsingh commented Feb 4, 2026

Implements the schema migration system from #519.

The main design choice here was creating a dedicated MigrationExpr type rather than modifying the existing SchemaExpr. The reason: SchemaExpr contains function-based types (IsNumeric, Optic[_, _, _]) that aren't serializable, and I didn't want to break or complicate existing code to make them work. A separate MigrationExpr keeps everything self-contained — all changes are in the migration package, nothing else is touched.

How it works

Two-layer architecture following the existing Patch/DynamicPatch pattern:

  • DynamicMigration — the untyped, serializable core. A vector of MigrationAction values (pure ADT, no closures) that operates on DynamicValue. Supports composition (++), structural reverse, and path-based navigation via DynamicOptic.

  • Migration[A, B] — typed wrapper that pairs a DynamicMigration with source/target schemas. Handles the conversion between typed values and DynamicValue.

  • MigrationBuilder — fluent API using macros to convert _.field selector syntax into DynamicOptic at compile time (both Scala 2.13 and 3). In Scala 3, there's also a MigrationComplete type class that gives you a compile error if your migration doesn't cover all fields.

  • MigrationExpr — pure expression type for computed values: arithmetic, string concat, comparisons, type conversions. All serializable, no closures.

Actions

20 action types covering records (AddField, DropField, Rename, TransformValue, Mandate, Optionalize, Nest, Unnest), joins/splits (Join, Split, JoinExpr, SplitExpr), type conversions (ChangeType, ChangeTypeExpr), enums (RenameCase, TransformCase), and collections (TransformElements, TransformKeys, TransformValues). Plus expression-based variants (TransformValueExpr).

Nested migrations work naturally — TransformCase, TransformElements, etc. contain their own Vector[MigrationAction], so you can express hierarchical transformations.

Quick example

// Compile-time validated (Scala 3)
val migration = Migration.builder[UserV1, UserV2]
  .renameField(_.name, _.fullName)
  .addField(_.age, 0)
  .build  // won't compile if fields are missing

// Nest fields into a sub-record
val nest = MigrationAction.Nest(root, "address", Vector("street", "city", "zip"))

// Expression-based transforms
val fullName = field(_.firstName) ++ Literal(" ") ++ field(_.lastName)

Tests

335 tests across 4 spec files, organized the same way the repo does it for Patch (separate PatchSpec, DynamicPatchSpec, etc.):

  • DynamicMigrationSpec — core ops, path navigation, Nest/Unnest, multi-step, laws
  • MigrationActionSpec — action laws, error handling, enum scenarios, reverse round-trips
  • MigrationBuilderSpec — typed Migration[A, B], builder API, composition
  • MigrationExprSpec — expression evaluation, arithmetic, string ops, type conversions

Also added DynamicMigrationBenchmark with 8 JMH benchmarks (addField, rename, composed, nested, sequence transform, reverse, nest, unnest) plus a spec that validates benchmark output.

Why 23 files

12 of the 23 files are platform-specific macro code for Scala 2/3 cross-compilation. The metaprogramming APIs are completely different between scala.reflect and scala.quoted, so each platform needs its own implementations. This is the same pattern used throughout the repo — Patch, Schema, Optic all have separate platform-specific files per concern.

The remaining 11 are: 5 shared source files (core logic), 4 test files, 2 benchmark files.

Closes #519

This implements the core Schema Migration System for ZIO Schema 2:

- MigrationAction ADT with 14 action types (AddField, DropField, Rename,
  TransformValue, Mandate, Optionalize, Join, Split, ChangeType,
  RenameCase, TransformCase, TransformElements, TransformKeys,
  TransformValues)
- DynamicMigration: pure, serializable migration operating on DynamicValue
- Migration[A, B]: typed wrapper with schema conversion
- MigrationBuilder: fluent API with macro-powered selectors
- Scala 3 selector macros converting _.field syntax to DynamicOptic
- Scala 2.13 selector macros for cross-version support
- Comprehensive test suite (13 tests)

Key features:
- All migrations are fully serializable (no closures)
- Path-based actions using DynamicOptic
- Structural reverse support
- Composition via ++ operator
- Error handling with path information
@yuvrajangadsingh yuvrajangadsingh force-pushed the feat/schema-migration-519-v2 branch from a8236d7 to 8aed127 Compare February 4, 2026 10:28
Implement full compile-time validation in the MigrationBuilder for Scala 3:

- Add MigrationComplete type class that validates migration completeness
- Add MigrationValidationMacros for extracting field names and validating
- Add MigrationBuilderMacros for type-level field tracking
- Update MigrationBuilder to use transparent inline methods with macros
- Track handled source fields and provided target fields at type level
- Auto-map fields with same name in source and target
- Add .build method that requires MigrationComplete evidence
- Keep .buildPartial for partial migrations without validation
- Add 5 new tests for MigrationBuilder functionality

The validation ensures all source fields are handled (renamed, dropped,
transformed) or auto-mapped, and all target fields are provided (added,
renamed to) or auto-mapped.
@yuvrajangadsingh yuvrajangadsingh force-pushed the feat/schema-migration-519-v2 branch from 4c2f654 to bf5cd81 Compare February 4, 2026 13:30
@yuvrajangadsingh yuvrajangadsingh force-pushed the feat/schema-migration-519-v2 branch from 059c2e5 to 7f046f3 Compare February 4, 2026 15:58
- Add 89 new tests (107 total) for MigrationAction types and edge cases
- Test all MigrationAction types: TransformValue, Mandate, Optionalize, ChangeType, RenameCase, TransformCase, TransformElements, TransformKeys, TransformValues, Join, Split
- Test error handling and path navigation for all node types
- Test fieldName/from exception branches for AddField, DropField, Rename
- Test error propagation in nested transformations
- Achieve 80.12% branch coverage (meeting 80% threshold)
@yuvrajangadsingh yuvrajangadsingh force-pushed the feat/schema-migration-519-v2 branch from 7f046f3 to 5a8a47f Compare February 4, 2026 16:40
yuvrajangadsingh and others added 9 commits February 4, 2026 23:46
Implement serializable expression system for migrations:

- Add MigrationExpr: pure, serializable expression type with:
  - Literal, FieldRef, StringConcat, Arithmetic operations
  - Type conversions (ToString, ToInt, ToLong, etc.)
  - Conditional expressions and comparisons
  - DSL operators for ergonomic usage

- Add expression-based MigrationActions:
  - TransformValueExpr: transform using MigrationExpr
  - ChangeTypeExpr: type conversion using expressions
  - JoinExpr: join fields with combine expression
  - SplitExpr: split field with multiple expressions

- Update MigrationBuilder (Scala 2 & 3):
  - transformFieldExpr(): expression-based field transforms
  - changeFieldTypeExpr(): expression-based type conversion
  - joinFields(): join with expressions
  - splitField(): split with expressions
  - Improve changeFieldType() implementation

- Add 56 new tests for MigrationExpr and expression actions
- Branch coverage: 80.57% (above 80% threshold)
Adds two tests from issue zio#519 success criteria:
- associativity law: (m1 ++ m2) ++ m3 == m1 ++ (m2 ++ m3)
- structural reverse law: m.reverse.reverse == m
Adds tests for MigrationExprOps DSL methods that were missing coverage:
- *, /, -, <, <=, >, >= operators
- toLong, toFloat, toDouble, toBigInt, toBigDecimal, toBoolean, asString conversions
Adds tests for:
- DSL === and =!= operators
- DSL toInt conversion
- MigrationExpr.field helper
- Convert ToString for various primitive types (Long, Double, Float, Boolean, BigInt, BigDecimal, Char)
Adds 16 more tests to improve branch coverage:
- SplitExpr reverse with combineExpr
- Arithmetic ops with Long, Double, Float
- BigInt/BigDecimal arithmetic error handling
- Convert ToInt/ToDouble/ToLong/ToFloat/ToBigInt/ToBigDecimal/ToBoolean
- Convert ToString for Short and Byte
Adds 20 more conversion tests for ToInt/ToLong/ToDouble/ToFloat
from various source types (Short, Byte, Float, Double, BigInt, BigDecimal)
to achieve better branch coverage on Scala 3.3.x
Adds 12 more tests for conversion coverage:
- ToBigInt from Short, Byte, Int
- ToBigDecimal from Short, Byte, Long, BigInt, String
- Error cases for unsupported conversions
- ToLong from Int
@yuvrajangadsingh yuvrajangadsingh marked this pull request as ready for review February 5, 2026 06:25
@yuvrajangadsingh
Copy link
Author

yuvrajangadsingh commented Feb 5, 2026

/claim #519

Ready for review! This implementation addresses all success criteria from issue #519 with:

  • 226 tests (most comprehensive test suite)
  • Zero changes to existing code - dedicated MigrationExpr instead of modifying SchemaExpr
  • Full compile-time validation via MigrationComplete type class (Scala 3)
  • Selector functions via macros (not strings) - addresses feedback from closed PRs
  • Hierarchical nested migrations (lists of lists) - addresses feedback from Schema Migration System for zio-blocks #891

cc @jdegoes

@yuvrajangadsingh yuvrajangadsingh marked this pull request as draft February 6, 2026 07:15
@yuvrajangadsingh yuvrajangadsingh marked this pull request as ready for review February 6, 2026 09:17
@yuvrajangadsingh
Copy link
Author

@copilot review

yuvrajangadsingh and others added 8 commits February 8, 2026 00:41
…d JMH benchmark

Split the 2853-line MigrationSpec.scala into 4 focused spec files
matching the repo's existing pattern (e.g., Patch module):
- DynamicMigrationSpec: core DynamicMigration, path navigation, edge cases
- MigrationActionSpec: action laws, error handling, field extraction
- MigrationBuilderSpec: typed Migration[A,B] and builder API
- MigrationExprSpec: expression system, arithmetic, executor paths

Added JMH benchmark for DynamicMigration operations:
- DynamicMigrationBenchmark: addField, rename, compose, nested, sequence
- DynamicMigrationBenchmarkSpec: validates benchmark output consistency

All 285 migration tests pass. No test logic changed.
Add Nest and Unnest migration actions for grouping/ungrouping fields
into sub-records. Useful for schema evolution like nesting street/city/zip
into an address sub-record.

New test suites:
- Nest/Unnest operations (11 tests)
- Multi-step migration scenarios (8 tests)
- Laws (identity, associativity, reverse-of-reverse)
- Deep path operations (4-level nesting, cross-type paths)
- Enum migration scenarios (6 tests)
- Reverse round-trip properties (5 tests)
- Builder edge cases and composition (10 tests)

Also added nestFields/unnestFields JMH benchmarks.
@jdegoes jdegoes closed this Feb 9, 2026
@yuvrajangadsingh
Copy link
Author

Hey @jdegoes — noticed this got closed. Is there specific feedback or something you'd like changed? Happy to rework if needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Schema Migration System for ZIO Schema 2

2 participants