Repository navigation
Per-codec CodecPlan; skip the reader mark for codecs that can't fail part-way (plan items 3.0 + 3.1) - #4398
Conversation
MorphiaCodec builds an immutable CodecPlan when it's created: the model's properties, their codecs by slot, and the properties to encode after the id. Each PropertyModel records its slot, its position in its model, when it's added, so a plan lookup is an array index. EntityEncoder, EntityDecoder and LifecycleDecoder read codecs from the plan of the codec for the model being encoded or decoded instead of from PropertyModel. The plan holds the same codecs MorphiaCodec has always set on the property models, so behavior is unchanged; it gives the Phase 3 items a per-codec home for codec-derived state.
There was a problem hiding this comment.
🟢 Approval recommended
The scoped refactor preserves existing codec resolution behavior, and no blocking issues remain.
0 open findings
What changed in this PR
Adds the per-codec plan foundation for Morphia’s encoding and decoding optimizations while retaining existing codec-sharing behavior.
Changes:
- Introduces property slots and a
CodecPlanwith ownership-checked codec lookup and precomputed encode order. - Routes encoding and decoding through plans, including lifecycle subtype decoding.
- Adds slot-lookup coverage and marks plan item 3.0 complete.
| File | Description |
|---|---|
| design/mapping-game-plan.md | Marks item 3.0 complete. |
| core/src/test/java/dev/morphia/test/mapping/codec/harness/CodecHarnessTest.java | Tests codec lookup and foreign-property rejection. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/PropertyModel.java | Stores each property’s slot. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/MorphiaCodec.java | Constructs and exposes its plan. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/LifecycleDecoder.java | Selects the resolved subtype’s plan. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java | Assigns slots when adding properties. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java | Uses plan codecs and encode order. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/EntityDecoder.java | Adds plan-aware decoding overloads. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java | Defines the slot-indexed codec plan. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Decoding a property took a reader mark before even checking for null, so a mismatched value could be rewound and decoded through the fallback. The NULL check now comes first, and CodecPlan holds a per-slot bitmask of the BSON types each property's codec reads directly: types it either reads completely or rejects before reading anything. When the current type is in the mask, the property decodes without a mark. The table is keyed on exact codec classes, so subclasses and user codecs keep the mark.
TestDirectBsonTypes walks CodecPlan's table: every listed codec class must be the one the codec registry resolves for its Java type, and must read every listed BSON type (boundary values included) completely without throwing. A driver upgrade that changes one of these codecs fails it.
|
Re the review's note that the fallback-sensitive optimization depends on driver behavior that wasn't independently verified: agreed, and ca24509 adds that verification as a test.
A table entry with no samples fails the test. I confirmed it catches a wrong entry: adding INT64 to |
Plan items 3.0 and 3.1, together, so the per-codec plan lands with its first real use.
3.0:
CodecPlanCodecPlan(@MorphiaInternal) is an immutable object thatMorphiaCodecbuilds once, in its constructor. It holds:encodeOrder, the properties encoded after the id;Why it exists. Codec-derived state used to live on
PropertyModel, which every datastore using the mapper shares, and it was set lazily. Most Phase 3 items cache something per property that depends on the codec (3.1's masks, 3.2's creator positions, 3.6's flags, 3.7's boxed types). The plan gives that state one per-codec home that's immutable once built, so it doesn't pile up as more lazily set fields on the shared model. In 4.1, codecs are resolved into it per datastore andPropertyModel.codecis removed.How properties are addressed. Each
PropertyModelrecords its slot, its position in its own model, assigned byEntityModel.addProperty. A plan lookup is therefore an array index.codec(property)rejects a property from another model with an identity check.Who reads it.
EntityEncoder,EntityDecoderandLifecycleDecoderread codecs from the plan of the codec for the model being encoded or decoded. WhenLifecycleDecoderresolves a subtype from the discriminator, it uses that subtype codec's plan. NewdecodeProperties(…, CodecPlan)anddecodeModel(…, CodecPlan, …)overloads carry the plan, and the old overloads delegate to them.The plan holds exactly the codecs
MorphiaCodecalready set on the property models, so what gets decoded is unchanged.3.1: skip the reader mark for codecs that can't fail part-way
Before, every decoded property took a reader mark (80 B) before even checking for null, so that a mismatched value could be rewound and decoded through the fallback. Now:
The table is keyed on the exact codec class, so subclasses and user codecs keep the mark:
StringCodecObjectIdCodecIntegerCodecLongCodecDoubleCodecBooleanCodecDecimal128Codec,BigDecimalCodecDateCodecEnumCodecMorphiaLocalDateTimeCodecMorphiaDateCodecHow the table was checked:
NumberCodecHelperbytecode in bson 5.13.0.decodeIntreads INT32 with no way to throw, but reads INT64, DOUBLE and DECIMAL128 first and then throws on precision loss, so those stay offIntegerCodec's list.decodeLonganddecodeDoublefollow the same pattern.EnumCodecon an unknown constant (IllegalArgumentException), was never caught by theBsonInvalidOperationExceptionfallback, so it propagates exactly as before.Tests
CodecHarnessTest.codecPlanResolvesEveryPropertyBySlotruns under both mappers. It checks that every property resolves to a codec, that the plan reports its model, and that a property of another model is rejected. It also checks the masks:StringCodecreads STRING directly but not INT32, andIntegerCodecreads INT32 directly but not INT64.TestDirectBsonTypespins the direct-type table against the real codecs. Each listed class must be what the registry resolves for its Java type, and must read every listed BSON type completely, boundary values included, without throwing. It fails for a wrong entry, and a driver upgrade that changes one of these codecs would fail it too.scalars-schema-driftandnested-single-value-for-listfixtures that exercise the fallback.-Dmorphia.mapper=critterand with reflection: 1485 + 5 tests, 0 failures.JMH:
CodecBenchmark,master(30554c3d456) vs this branchSame machine as the baseline (i9-11900H, Temurin 17),
-prof gc, the benchmark's own settings (3 forks × 5 × 1 s). Before and after ran back to back with nothing else running. Critter is the AOT variant.reflection
critter
Reading it:
decodeInCursortoo.encodeOrderis an array: 32 B on SIMPLE, up to 736 B on critter POLYMORPHIC. Encode time is flat.decodeInCursor SIMPLE) didn't reproduce on two reruns.Decisions to review
PropertyModel. As before, the first datastore to build a property's codec supplies it to every later one. Resolving codecs per datastore, and deletingPropertyModel.codec, is 4.1's job.PropertyModel. It's structural: the property's position in its own model, fixed once added and the same for every datastore. That makes lookups array indexes instead of identity-map lookups per field.TestDirectBsonTypesfails if a driver upgrade changes one of these codecs' read behavior. I addedBigDecimalCodec, which the review didn't list but which reads DECIMAL128 the same wayDecimal128Codecdoes.CodecBenchmarkunder reflection and critter (AOT) only. critter-runtime runs the same decode path.