Skip to content

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

Open
987Nabil wants to merge 41 commits intomainfrom
schema-migration-system-519
Open

feat(schema): implement Schema Migration System (#519)#966
987Nabil wants to merge 41 commits intomainfrom
schema-migration-system-519

Conversation

@987Nabil
Copy link
Contributor

@987Nabil 987Nabil commented Feb 4, 2026

Summary

Complete migration system with fully serializable expressions and ergonomic builder APIs.

Key Features

Feature Description
DynamicSchemaExpr Fully serializable expression ADT (21 case classes) - no Schema[A] embedded
SchemaExpr wrapper SchemaExpr[A, B](dynamic, inputSchema, outputSchema) for type safety
Nested field tracking Dot notation paths (FieldName["address.street"]) via intersection types
Migration composition .migrateField(src, tgt, migration) with explicit/implicit resolution
Ergonomic literal API literal(1000) with implicit Schema, not verbose DynamicValue construction
MigrationBuilder syntax Primary API for all migrations with selector syntax

API Examples

Ergonomic literals:

// Before (verbose)
literal(DynamicValue.Primitive(PrimitiveValue.Int(1000)))

// After (ergonomic)  
literal(1000)

MigrationBuilder with selectors:

val migration = Migration
  .newBuilder[PersonV1, PersonV2]
  .addField(_.age, literal(0))
  .renameField(_.firstName, _.fullName)
  .build

Nested field migration:

val migration = Migration
  .newBuilder[PersonV1, PersonV2]
  .transformNested(_.address, _.address) { builder =>
    builder.addField(_.zip, literal("00000"))
  }
  .build

Migration composition:

// Explicit
.migrateField(_.user, _.user, userMigration)

// Implicit - summons Migration[User, UserV2] from scope
.migrateField(_.user, _.user)

Success Criteria Checklist

  • DynamicMigration fully serializable (no closures, all data)
  • Migration[A, B] wraps schemas and actions
  • All actions path-based via DynamicOptic
  • User API uses selector functions (S => A) for "optics" on old and new types
  • Macro validation in .build to confirm "old" has been migrated to "new"
  • .buildPartial supported
  • Structural reverse implemented
  • Identity & associativity laws hold
  • Enum rename / transform supported
  • Errors include path information
  • Comprehensive tests (191 tests)
  • Scala 2.13 and Scala 3.x supported
  • Type-level field tracking with intersection types (unified Scala 2/3 API)
  • 88.53% statement coverage / 84.34% branch coverage

Test Coverage

  • 191 tests in MigrationSpec
  • 88.53% statement coverage / 84.34% branch coverage
  • Nested type migration tests (2-4 level nesting)
  • Error message tests with explicit assertions
  • Only 1 test uses Migration.fromActions (as API proof)

Key Components

Component Description
DynamicMigration Untyped interpreter: applies MigrationActions to DynamicValue
Migration[A, B] Typed wrapper with apply / applyDynamic / reverse / ++
MigrationAction 17-variant ADT (AddField, DropField, Rename, TransformValue, TransformNested, ApplyMigration, ...)
DynamicSchemaExpr Serializable expression language (Literal, Select, Arithmetic, StringOps, ...)
SchemaExpr[A, B] Typed wrapper around DynamicSchemaExpr
MigrationBuilder Fluent builder with build (validates) / buildPartial (skips validation)
Selector + macros Compile-time selector lambdas (_.address.street) to DynamicOptic paths

Files Changed

File Purpose
DynamicSchemaExpr.scala Serializable expression ADT
SchemaExpr.scala Typed wrapper
MigrationAction.scala Added TransformNested, ApplyMigration
DynamicMigration.scala Execute new actions
MigrationBuilder.scala (Scala 2 & 3) transformNested, migrateField
MigrationSpec.scala Comprehensive tests (191)

Closes #519

Copilot AI review requested due to automatic review settings February 4, 2026 06:15
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive Schema Migration System for ZIO Schema 2, enabling pure, serializable schema migrations with type-safe selector syntax. The implementation provides 14 migration actions for records, enums, and collections, along with extensive test coverage across multiple test suites.

Changes:

  • Implemented core migration system with DynamicMigration, Migration[A, B], and 14 serializable action types
  • Added type-safe builder API with macro-validated selector expressions (S => A syntax)
  • Created comprehensive test suites covering Into/As conversions, validation, structural types, and edge cases

Reviewed changes

Copilot reviewed 106 out of 106 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
selector-syntax-summary.md Documents selector syntax implementation and infrastructure
schema/shared/src/test/scala/* Extensive test coverage for Into/As conversions, validation, and type coercion
schema/shared/src/test/scala-3/* Scala 3-specific tests for opaque types, enums, and large tuples
schema/shared/src/test/scala-2/* Scala 2-specific tests for ZIO Prelude newtypes
schema/shared/src/main/scala/zio/blocks/schema/migration/* Core migration system implementation
schema/shared/src/main/scala/zio/blocks/schema/* Extended PrimitiveType, SchemaError, IsNumeric for migration support
docs/reference/structural-schemas.md Comprehensive documentation for structural schema support
IMPLEMENTATION_INDEX.md, COMPLETION_REPORT.md, 519-implementation-plan.md Implementation planning and completion tracking documents

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

at: DynamicOptic,
transform: SchemaExpr[?, ?]
) extends MigrationAction {
override def reverse: MigrationAction = TransformValue(at, transform) // Note: true reverse needs inverse function
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment indicates "true reverse needs inverse function" but the implementation returns the same transform. This creates a structural reverse that may not be semantically correct. Consider adding a TODO or making this limitation more explicit in the documentation.

Suggested change
override def reverse: MigrationAction = TransformValue(at, transform) // Note: true reverse needs inverse function
// NOTE: This is a structural reverse that re-applies the same transform.
// TODO: Support specifying an inverse transform for true semantic reversal.
// Until then, reverse may not be a real inverse for all transformations.
override def reverse: MigrationAction = TransformValue(at, transform)

Copilot uses AI. Check for mistakes.
SchemaExpr.literal(value)

def spec: Spec[TestEnvironment, Any] = suite("MigrationSpec")(
suite("DynamicMigration")(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull DynamicMigration out into its own spec.


val migration = Migration
.newBuilder[PersonV1, PersonV2]
.addField(_.age, literalExpr(0))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would hope we can just do literal or SchemaExpr.literal rather than literalExpr.


val migration = Migration
.newBuilder[PersonV1, PersonV2]
.changeFieldType(_.score, _.score, literalExpr("42"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this take two selectors? To prove the same field exists in both?


val migration = Migration
.newBuilder[PersonV1, PersonV2]
.addField(_.age, literal(DynamicValue.Primitive(PrimitiveValue.Int(0))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be literalInt and not literal(dynamicvalue).

val migration = Migration
.newBuilder[PersonV1, PersonV2]
.transformNested(_.address, _.address) { builder =>
builder.addField(_.zip, literal(DynamicValue.Primitive(PrimitiveValue.String("00000"))))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these literal(dynamicvalues) should be replaced by literal[T](t: T), which eagerly converts to dynamic value.

@987Nabil 987Nabil force-pushed the schema-migration-system-519 branch from eda5a17 to 06782b6 Compare February 26, 2026 13:56
@987Nabil 987Nabil force-pushed the schema-migration-system-519 branch 3 times, most recently from af7ab9c to efde36b Compare March 14, 2026 11:05
// By wrapping in FieldName[N], the type argument is preserved when using appliedTo.
sealed trait FieldName[N <: String & Singleton]

final class MigrationBuilder[A, B, SourceHandled, TargetProvided](
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of SourceHandled, TargetProvided keep one param, that tracks all the actions that did apply to A to become B encoded in the type system. So the type is the full change set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think of the third type parameter as Changeset, which contains at type-level encoding of all the changes that have been applied to A to bring it into comformance with B -- each individual operation being one of those methods on MigrationBuilder.

The macros need a structural encoding of both A and B, and then need a way to apply the changeset to A, so you can check conformance with B.

Implements a pure, serializable migration system for ZIO Schema 2.

Key features:
- DynamicMigration: fully serializable (no closures), all transforms as SchemaExpr
- Migration[A, B]: typed wrapper with schemas
- 14 MigrationAction types (AddField, DropField, Rename, Transform, etc.)
- MigrationBuilder with selector macros for Scala 2 and 3
- Macro validation in .build to confirm old→new migration
- Structural reverse and semantic inverse laws
- Enum rename/transform support

Additional changes required for serialization:
- SchemaExpr: made fully serializable (NumericPrimitiveType, DynamicOptic)
- IsIntegral: new type class for mixed-type bitwise operations
- Optic: added bitwise DSL methods

Tests: 84+ tests across MigrationSpec, StructuralMigrationSpec, MigrationBuildValidationSpec

Closes #519
Complete migration of StringReplace, StringStartsWith, StringEndsWith, StringContains, and StringIndexOf classes to use the new eval[B1 >: CONCRETE_TYPE] signature. All classes now match the updated SchemaExpr trait interface and successfully compile with all tests passing.
- Add andThen composition test
- Add size utility method test
- Add rename field not found error test
- Add rename field already exists error test
- Add renameCase with non-matching case name test
- Add 20+ new tests in 'Error Branches' suite
- Cover type mismatch errors (AddField, DropField, Rename on non-Record)
- Cover variant errors (RenameCase, TransformCase on non-Variant)
- Cover collection errors (TransformElements, TransformKeys, TransformValues)
- Cover path navigation errors (Case mismatch, AtIndex bounds, AtMapKey missing)
- Cover nested operation failures
- Test isEmpty method on empty and non-empty migrations
- Add 13 more tests covering edge cases and error branches
- Test Node.Wrapped path navigation
- Test early failure in multi-action migrations
- Test empty collection edge cases
- Test deep nested record operations
- Total: 112 migration tests
…AtPath node types

- Add tests for all 14 action type reversals (AddField↔DropField, Rename, etc)
- Add tests for DynamicMigration varargs constructor with 1-3 actions
- Add tests for all modifyAtPath node types (Field, Case, Elements, MapKeys, MapValues, AtIndex, AtMapKey, Wrapped)
- Add tests for Mandate/Optionalize edge cases and round-trips
- Add tests for reverse composition and double-reverse preservation
- Add tests for Join/Split reversal pairs
- Increase branch coverage from 78.86% to 100% on Scala 3.3.x
- All 142 tests pass on Scala 3.7.4, 3.3.7, and 2.13.18
Add tests for builder methods:
- changeFieldType selector syntax
- mandateField builder (creates Mandate action)
- optionalizeField builder (creates Optionalize action)
- transformElements builder (creates TransformElements action)
- transformKeys builder (creates TransformKeys action)
- transformValues builder (creates TransformValues action)
- renameCase builder (creates RenameCase action)

These tests verify the builder API creates correct actions without
testing execution (which has existing coverage). This should help
reach the 80% branch coverage target on Scala 3.3.x.
Cover the error paths in MigrationAction's fieldName and from methods:
- AddField.fieldName success path
- AddField.fieldName failure path (root only, non-Field node)
- DropField.fieldName success path
- DropField.fieldName failure path (root only, non-Field node)
- Rename.from success path
- Rename.from failure path (root only, non-Field node)

These tests exercise branches previously not covered to help reach
the 80% branch coverage target.
987Nabil added 25 commits March 19, 2026 09:26
…ieldName wrapper

- Remove AddField type alias (no longer needed)
- Add documentation explaining why FieldName wrapper is necessary
- FieldName preserves literal types through macro expansion
- Direct ConstantType with .asType causes literal types to widen to String
- All tests pass on Scala 3.7.4 and 3.3.7
…in Scala 2

- Add FieldName[N] phantom type wrapper for literal type preservation
- Update MigrationBuilder signature to [A, B, SourceHandled, TargetProvided]
- All macro methods now return refined types with accumulated field names
- Add MigrationComplete trait with implicit macro derivation
- build method requires MigrationComplete evidence
- buildPartial unchanged (no validation)
- All tests pass on Scala 2.13.18
In Scala 2, the macro keyword must be followed immediately by the
implementation reference on the same line. Scalafmt was breaking
this by putting line breaks between '= macro' and the implementation.

Added format:off/on directives to preserve correct macro syntax.
- Fully serializable expression ADT that operates on DynamicValue
- No type parameters, no Schema references, no function closures
- Implements all 21 case classes from design document:
  - Leaf expressions: Literal, Select, PrimitiveConversion
  - Relational operators: LessThan, GreaterThan, Equal, NotEqual, etc.
  - Logical operators: And, Or, Not
  - Arithmetic operators with NumericTypeTag (replaces NumericPrimitiveType)
  - Bitwise operators: And, Or, Xor, LeftShift, RightShift, etc.
  - String operations: Concat, Length, Substring, Trim, ToUpperCase, etc.
- Select implements path walking logic adapted from SchemaExpr.Optic
- NumericTypeTag sealed trait provides serializable arithmetic operations
- All operators implemented as sealed traits of case objects
- Compiles on both Scala 3.7.4 and Scala 2.13.18
…aExpr

- Convert SchemaExpr from sealed trait hierarchy to single case class wrapper
- Add DynamicSchemaExpr as serializable expression ADT (no Schema[A] embedded)
- Implement typed eval/evalDynamic methods that convert via DynamicValue
- Fix type shadowing (Validation.String vs Predef.String) throughout codebase
- Add PrimitiveType.toTag implementations for all primitive types
- Update test literal helpers to use new typed factory methods
- Remove structural test assertions (wrapper pattern hides internal structure)

Main code compiles on both Scala 3.7.4 and 2.13.15.
4 expected test failures in OpticSpec related to error-checking (future task).

This refactoring enables:
- Full serialization of schema expressions (DynamicSchemaExpr)
- Type-safe expression evaluation with compile-time checking
- Clean separation of serializable ADT from typed wrapper
- Backward compatibility via dual eval interfaces
…maExpr

- Add proper error handling in DynamicSchemaExpr.Select.walkPath for:
  - UnexpectedCase errors for wrong enum variants
  - EmptySequence errors for empty collections
  - SequenceIndexOutOfBounds for invalid indices
  - MissingKey and EmptyMap for map operations
- Convert SchemaError to OpticCheck in SchemaExpr.eval/evalDynamic
- Fix StringRegexMatch to use string's input schema instead of regex's
- Add TransformNested action for nested record migrations
- Add transformNested method to MigrationBuilder (Scala 2 & 3)
- Track nested fields with dot notation (e.g., FieldName["address.street"])
- Update validation macros to check nested field completeness
- Add tests for nested type migrations
Add migrateField method to MigrationBuilder that allows composing
existing migrations for nested types. This enables reusing migrations
when migrating complex nested structures.

Features:
- .migrateField(src, tgt, migration) with explicit migration parameter
- .migrateField(src, tgt) summons implicit/given Migration from scope
- All nested fields from composed migration are expanded into type tracking
- New MigrationAction.ApplyMigration for runtime execution
- Works with both Scala 2 and Scala 3
Add 9 new migration tests covering:
- 2-4 level nested case class migrations with field operations
- Collection-wrapped nested types (Option, List, Map)
- Recursive type migrations with self-referential fields
- Multiple operations on nested types in single migration

Also document migration test patterns, limitations, and best practices
in docs/learnings.md. Key learnings:
- DynamicMigration cannot traverse Variant types (Option wrappers)
- Collection migrations work via identity + automatic type conversion
- Recursive schemas require 'implicit lazy val' to prevent forward refs
- Deeply nested paths (4+ levels) work correctly with dot notation

All 191 tests passing on Scala 3.7.4 (including 9 new tests).
… syntax in tests

- Replace verbose DynamicValue.Primitive(PrimitiveValue.X(...)) with simple literal(value)
- Use MigrationBuilder selector syntax instead of Migration.fromActions
- Keep one Migration.fromActions test as proof it works
…plit tests

- Remove transformNested API entirely (deep selectors already work)
- Simplify migrateField to ONE selector + migration (not two selectors)
- Fix literal naming (literalExpr -> literal) in tests
- Fix MigrationBuildValidationSpec to use ergonomic literal(0)
- Split DynamicMigration tests into DynamicMigrationSpec
- Remove TransformNested from MigrationAction
- Remove executeTransformNested from DynamicMigration
- All 203 tests pass, Scala 2.13 + 3.7.4 compile
Enable chained field selectors like _.address.street in all builder
methods (addField, dropField, renameField, transformField, etc.).
The macro now extracts full dot-separated paths instead of only the
leaf field name, and the validation macros support cross-type auto-
mapping of matching nested fields when the user manipulates individual
nested fields directly.
Add 3-level deep nesting tests (Person→Address→Street), combined
rename+drop at depth 3, fromActions with deep DynamicOptic paths,
and phantom type inference verification via typeCheck. Fix validation
macros to recurse cross-type auto-mapping and use fixpoint iteration
for parent coverage inference at arbitrary nesting depths.
SchemaExpr was refactored from a sealed ADT to a case class wrapping
DynamicSchemaExpr, but schema-examples and docs were not updated. This
caused 115 compilation errors on Scala 3.x CI and 48 mdoc errors on
Build Docs CI.

- Update querydslsql, querydslextended, querydslbuilder examples to
  pattern match on expr.dynamic (DynamicSchemaExpr) instead of the old
  SchemaExpr ADT cases
- Update Column to hold DynamicOptic instead of typed Optic
- Update Lit to hold DynamicValue instead of typed value
- Add sqlLiteralDV helper for rendering DynamicValue to SQL
- Fix SchemaErrorExample.scala import (JsonFormat -> JsonBinaryCodecDeriver)
- Update all 4 query DSL guide docs and schema-expr reference docs to
  match new API structure
Add missing algebraic law tests for both typed Migration and DynamicMigration:
- Identity element under composition (left and right) for both layers
- Typed Migration associativity, double-reverse involution, semantic inverse
- MaxPathDepth=64 guard in ActionExecutor.modifyAtPath to prevent stack overflow
…tinel for non-invertible reverse

Convert all 18 throw sites in DynamicSchemaExpr eval methods to proper
Left(SchemaError.conversionFailed(...)) returns with shared extractors.

Add Irreversible sentinel MigrationAction so reversing non-invertible
actions (TransformValue, ChangeType, TransformElements, TransformKeys,
TransformValues, Join, Split) now fails with a clear error instead of
silently re-applying the forward transform.

Clarify TransformElements/Keys/Values Scaladoc to document replace-all
semantics.
… Changeset type param

Replace MigrationBuilder[A, B, SourceHandled, TargetProvided] with
MigrationBuilder[A, B, Changeset] where the Changeset encodes all
operations (Added, Dropped, Renamed, Transformed, etc.) as phantom
types in an intersection. Both Scala 2 and 3 updated.
Verify each builder operation accumulates the correct phantom type
in the Changeset parameter (Added, Dropped, Renamed, Transformed)
using typeCheck with forced type mismatch to reveal inferred types.
@987Nabil 987Nabil force-pushed the schema-migration-system-519 branch from efde36b to cacb259 Compare March 19, 2026 09:21
@987Nabil 987Nabil requested a review from Copilot March 19, 2026 09:45
Add missing match cases for new DynamicOptic.Node variants added
in main (SchemaSearch, TypeSearch). These are not supported in
schema expressions and return appropriate errors.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 45 out of 46 changed files in this pull request and generated 10 comments.

Comment on lines +137 to +140
override def reverse: MigrationAction = Mandate(
at,
DynamicSchemaExpr.Literal(DynamicValue.Record.empty)
)
Comment on lines +5 to +99
/**
* A typed migration that transforms values from type `A` to type `B`.
*
* `Migration[A, B]` provides a type-safe wrapper around `DynamicMigration`,
* handling the conversion between typed values and `DynamicValue`
* automatically.
*
* The migration process:
* 1. Convert input `A` to `DynamicValue` using `sourceSchema`
* 2. Apply the `dynamicMigration` to transform the dynamic value
* 3. Convert the result back to `B` using `targetSchema`
*
* The schemas can be any `Schema[A]` and `Schema[B]`, including:
* - Regular schemas derived from case classes/enums
* - Structural schemas (JVM only) for compile-time-only types
*
* This allows migrations between current runtime types and past structural
* versions without requiring old case classes to exist at runtime.
*
* @tparam A
* The source type
* @tparam B
* The target type
* @param sourceSchema
* Schema for the source type (can be structural or regular)
* @param targetSchema
* Schema for the target type (can be structural or regular)
* @param dynamicMigration
* The underlying untyped migration
*/
final case class Migration[A, B](
sourceSchema: Schema[A],
targetSchema: Schema[B],
dynamicMigration: DynamicMigration
) {

/**
* Apply this migration to transform a value from type `A` to type `B`.
*
* @param value
* The input value to migrate
* @return
* Either a `SchemaError` or the migrated value
*/
def apply(value: A): Either[SchemaError, B] = {
val dynamicValue = sourceSchema.toDynamicValue(value)
dynamicMigration(dynamicValue).flatMap { result =>
targetSchema.fromDynamicValue(result)
}
}

/**
* Compose this migration with another, applying this migration first, then
* the other.
*
* @param that
* The migration to apply after this one
* @return
* A new migration that applies both in sequence
*/
def ++[C](that: Migration[B, C]): Migration[A, C] =
new Migration(sourceSchema, that.targetSchema, dynamicMigration ++ that.dynamicMigration)

/**
* Alias for `++`.
*/
def andThen[C](that: Migration[B, C]): Migration[A, C] = this ++ that

/**
* Returns the structural reverse of this migration.
*
* Note: Runtime execution of the reverse migration is best-effort. It may
* fail if information was lost during the forward migration.
*/
def reverse: Migration[B, A] =
new Migration(targetSchema, sourceSchema, dynamicMigration.reverse)

/**
* Returns true if this migration has no actions (identity migration).
*/
def isEmpty: Boolean = dynamicMigration.isEmpty

/**
* Returns the number of actions in this migration.
*/
def size: Int = dynamicMigration.size

/**
* Get the list of actions in this migration.
*/
def actions: Vector[MigrationAction] = dynamicMigration.actions
}

object Migration extends MigrationSelectorSyntax with MigrationCompanionVersionSpecific {

Comment on lines +75 to +99
suite("Relational")(
test("LessThan operator") {
val _ = SchemaExpr.relational(intLit(5), intLit(10), SchemaExpr.RelationalOperator.LessThan)
assertTrue(true)
},
test("GreaterThan operator") {
val _ = SchemaExpr.relational(intLit(10), intLit(5), SchemaExpr.RelationalOperator.GreaterThan)
assertTrue(true)
},
test("LessThanOrEqual operator") {
val _ = SchemaExpr.relational(intLit(5), intLit(5), SchemaExpr.RelationalOperator.LessThanOrEqual)
assertTrue(true)
},
test("GreaterThanOrEqual operator") {
val _ = SchemaExpr.relational(intLit(10), intLit(10), SchemaExpr.RelationalOperator.GreaterThanOrEqual)
assertTrue(true)
},
test("Equal operator") {
val _ = SchemaExpr.relational(intLit(42), intLit(42), SchemaExpr.RelationalOperator.Equal)
assertTrue(true)
},
test("NotEqual operator") {
val _ = SchemaExpr.relational(intLit(42), intLit(99), SchemaExpr.RelationalOperator.NotEqual)
assertTrue(true)
}
Comment on lines +130 to +150
transparent inline def migrateField[F1, F2](
inline selector: A => F1,
migration: Migration[F1, F2]
) = ${
MigrationBuilderMacros.migrateFieldExplicitImpl[A, B, F1, F2, Changeset](
'this,
'selector,
'migration
)
}

@scala.annotation.targetName("migrateFieldImplicit")
transparent inline def migrateField[F1, F2](
inline selector: A => F1
)(using migration: Migration[F1, F2]) = ${
MigrationBuilderMacros.migrateFieldImplicitImpl[A, B, F1, F2, Changeset](
'this,
'selector,
'migration
)
}
Comment on lines +111 to +392
suite("Not")(
test("negates boolean value") {
val _ = SchemaExpr.not(boolLit(true))
assertTrue(true)
}
),
suite("Not")(
test("negates boolean expression") {
val _ = SchemaExpr.not(boolLit(true))
assertTrue(true)
}
),
suite("Arithmetic")(
test("Add operator") {
val _ =
SchemaExpr.arithmetic(intLit(10), intLit(5), SchemaExpr.ArithmeticOperator.Add, PrimitiveType.Int(None))
assertTrue(true)
},
test("Subtract operator") {
val _ =
SchemaExpr.arithmetic(intLit(10), intLit(5), SchemaExpr.ArithmeticOperator.Subtract, PrimitiveType.Int(None))
assertTrue(true)
},
test("Multiply operator") {
val _ =
SchemaExpr.arithmetic(intLit(10), intLit(5), SchemaExpr.ArithmeticOperator.Multiply, PrimitiveType.Int(None))
assertTrue(true)
},
test("Divide operator") {
val _ =
SchemaExpr.arithmetic(intLit(10), intLit(2), SchemaExpr.ArithmeticOperator.Divide, PrimitiveType.Int(None))
assertTrue(true)
},
test("Pow operator") {
val _ =
SchemaExpr.arithmetic(intLit(2), intLit(3), SchemaExpr.ArithmeticOperator.Pow, PrimitiveType.Int(None))
assertTrue(true)
},
test("Modulo operator") {
val _ =
SchemaExpr.arithmetic(intLit(10), intLit(3), SchemaExpr.ArithmeticOperator.Modulo, PrimitiveType.Int(None))
assertTrue(true)
}
),
suite("Bitwise")(
test("And operator (Int)") {
val _ = SchemaExpr.bitwise(intLit(12), intLit(10), SchemaExpr.BitwiseOperator.And)
assertTrue(true)
},
test("Or operator (Int)") {
val _ = SchemaExpr.bitwise(intLit(12), intLit(10), SchemaExpr.BitwiseOperator.Or)
assertTrue(true)
},
test("Xor operator (Int)") {
val _ = SchemaExpr.bitwise(intLit(12), intLit(10), SchemaExpr.BitwiseOperator.Xor)
assertTrue(true)
},
test("LeftShift operator (Int)") {
val _ = SchemaExpr.bitwise(intLit(5), intLit(2), SchemaExpr.BitwiseOperator.LeftShift)
assertTrue(true)
},
test("RightShift operator (Int)") {
val _ = SchemaExpr.bitwise(intLit(20), intLit(2), SchemaExpr.BitwiseOperator.RightShift)
assertTrue(true)
},
test("UnsignedRightShift operator (Int)") {
val _ = SchemaExpr.bitwise(intLit(20), intLit(2), SchemaExpr.BitwiseOperator.UnsignedRightShift)
assertTrue(true)
},
test("And operator (Byte)") {
val expr = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.And)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with byte and byte (And)") {
val expr = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.And)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with byte and byte (Or)") {
val expr = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.Or)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with byte and byte (Xor)") {
val expr = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.Xor)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with short and short (And)") {
val expr = SchemaExpr.bitwise(shortLit(12), shortLit(10), SchemaExpr.BitwiseOperator.And)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with short and short (Or)") {
val expr = SchemaExpr.bitwise(shortLit(12), shortLit(10), SchemaExpr.BitwiseOperator.Or)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with int and int (Xor)") {
val expr = SchemaExpr.bitwise(intLit(12), intLit(10), SchemaExpr.BitwiseOperator.Xor)
val result = expr.evalDynamic(())
assertTrue(result.isRight && result.exists(seq => seq.nonEmpty))
},
test("evalDynamic with byte and byte (And)") {
val _ = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.And)
assertTrue(true)
},
test("evalDynamic with byte and byte (Or)") {
val _ = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.Or)
assertTrue(true)
},
test("evalDynamic with byte and byte (Xor)") {
val _ = SchemaExpr.bitwise(byteLit(12), byteLit(10), SchemaExpr.BitwiseOperator.Xor)
assertTrue(true)
},
test("evalDynamic with short and short (And)") {
val _ = SchemaExpr.bitwise(shortLit(12), shortLit(10), SchemaExpr.BitwiseOperator.And)
assertTrue(true)
},
test("evalDynamic with short and short (Or)") {
val _ = SchemaExpr.bitwise(shortLit(12), shortLit(10), SchemaExpr.BitwiseOperator.Or)
assertTrue(true)
},
test("evalDynamic with int and int (Xor)") {
val _ = SchemaExpr.bitwise(intLit(12), intLit(10), SchemaExpr.BitwiseOperator.Xor)
assertTrue(true)
},
test("evalDynamic with long and long (LeftShift)") {
val _ = SchemaExpr.bitwise(longLit(12), longLit(2), SchemaExpr.BitwiseOperator.LeftShift)
assertTrue(true)
},
test("evalDynamic with long and long (RightShift)") {
val _ = SchemaExpr.bitwise(longLit(12), longLit(2), SchemaExpr.BitwiseOperator.RightShift)
assertTrue(true)
},
test("evalDynamic with long and long (UnsignedRightShift)") {
val _ = SchemaExpr.bitwise(longLit(12), longLit(2), SchemaExpr.BitwiseOperator.UnsignedRightShift)
assertTrue(true)
}
),
suite("BitwiseNot")(
test("negates int value") {
val _ = SchemaExpr.bitwiseNot(intLit(42))
assertTrue(true)
},
test("evalDynamic returns negated value") {
val _ = SchemaExpr.bitwiseNot(byteLit(5))
assertTrue(true)
}
),
suite("StringConcat")(
test("concatenates two strings") {
val _ = SchemaExpr.stringConcat(stringLit("hello"), stringLit("world"))
assertTrue(true)
}
),
suite("StringRegexMatch")(
test("matches regex pattern") {
val _ = SchemaExpr.stringRegexMatch(stringLit("[a-z]+"), stringLit("hello"))
assertTrue(true)
}
),
suite("StringLength")(
test("gets string length") {
val _ = SchemaExpr.stringLength(stringLit("hello"))
assertTrue(true)
}
),
suite("StringSubstring")(
test("extracts substring") {
val _ = SchemaExpr.stringSubstring(stringLit("hello"), intLit(0), intLit(3))
assertTrue(true)
}
),
suite("StringTrim")(
test("trims whitespace") {
val _ = SchemaExpr.stringTrim(stringLit(" hello "))
assertTrue(true)
}
),
suite("StringToUpperCase")(
test("converts to uppercase") {
val _ = SchemaExpr.stringToUpperCase(stringLit("hello"))
assertTrue(true)
}
),
suite("StringToLowerCase")(
test("converts to lowercase") {
val _ = SchemaExpr.stringToLowerCase(stringLit("HELLO"))
assertTrue(true)
}
),
suite("StringReplace")(
test("replaces target with replacement") {
val _ = SchemaExpr.stringReplace(stringLit("hello world"), stringLit("world"), stringLit("scala"))
assertTrue(true)
}
),
suite("StringStartsWith")(
test("checks if string starts with prefix") {
val _ = SchemaExpr.stringStartsWith(stringLit("hello"), stringLit("he"))
assertTrue(true)
}
),
suite("StringEndsWith")(
test("checks if string ends with suffix") {
val _ = SchemaExpr.stringEndsWith(stringLit("hello"), stringLit("lo"))
assertTrue(true)
}
),
suite("StringContains")(
test("checks if string contains substring") {
val _ = SchemaExpr.stringContains(stringLit("hello world"), stringLit("world"))
assertTrue(true)
}
),
suite("StringConcat")(
test("concatenates two strings") {
val _ = SchemaExpr.stringConcat(stringLit("hello"), stringLit("world"))
assertTrue(true)
}
),
suite("StringRegexMatch")(
test("matches regex pattern") {
val _ = SchemaExpr.stringRegexMatch(stringLit("[a-z]+"), stringLit("hello"))
assertTrue(true)
}
),
suite("StringLength")(
test("gets string length") {
val _ = SchemaExpr.stringLength(stringLit("hello"))
assertTrue(true)
}
),
suite("StringSubstring")(
test("extracts substring") {
val _ = SchemaExpr.stringSubstring(stringLit("hello"), intLit(0), intLit(3))
assertTrue(true)
}
),
suite("StringTrim")(
test("trims whitespace") {
val _ = SchemaExpr.stringTrim(stringLit(" hello "))
assertTrue(true)
}
),
suite("StringToUpperCase")(
test("converts to uppercase") {
val _ = SchemaExpr.stringToUpperCase(stringLit("hello"))
assertTrue(true)
}
),
suite("StringToLowerCase")(
test("converts to lowercase") {
val _ = SchemaExpr.stringToLowerCase(stringLit("HELLO"))
assertTrue(true)
}
),
suite("StringReplace")(
test("replaces target with replacement") {
val _ = SchemaExpr.stringReplace(stringLit("hello world"), stringLit("world"), stringLit("scala"))
assertTrue(true)
}
),
suite("StringStartsWith")(
test("checks if string starts with prefix") {
val _ = SchemaExpr.stringStartsWith(stringLit("hello"), stringLit("he"))
assertTrue(true)
}
),
suite("StringEndsWith")(
test("checks if string ends with suffix") {
val _ = SchemaExpr.stringEndsWith(stringLit("hello"), stringLit("lo"))
assertTrue(true)
}
),
suite("StringContains")(
test("checks if string contains substring") {
val _ = SchemaExpr.stringContains(stringLit("hello world"), stringLit("world"))
assertTrue(true)
}
Comment on lines +398 to +402
assertTrue(
result.isLeft,
result.swap.toOption.get.contains("Added[(\"age\" : String)]")
)
}
Comment on lines +439 to +443
assertTrue(
result.isLeft,
result.swap.toOption.get.contains("Renamed[(\"name\" : String), (\"fullName\" : String)]")
)
}
Comment on lines +459 to +463
}.map { result =>
assertTrue(
result.isLeft,
result.swap.toOption.get.contains("Transformed[(\"age\" : String), (\"age\" : String)]")
)
Comment on lines +483 to +487
assertTrue(
result.isLeft,
result.swap.toOption.get.contains("Renamed[(\"name\" : String), (\"fullName\" : String)]"),
result.swap.toOption.get.contains("Added[(\"email\" : String)]")
)
Comment on lines +45 to +48
def fieldName: String = at.nodes.lastOption match {
case Some(DynamicOptic.Node.Field(name)) => name
case _ => throw new IllegalStateException("AddField path must end with a Field node")
}
…rsible, test cleanup

- Optionalize.reverse now returns Irreversible (empty Record default was broken)
- fieldName/from methods return Option[String] instead of throwing
- Replace unsafe .swap.toOption.get with .left.exists in type inference tests
- Remove 56 no-op assertTrue(true) tests and deduplicate suites in SchemaExprSpec
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

3 participants