diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java
new file mode 100644
index 00000000000..cd69976ba94
--- /dev/null
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java
@@ -0,0 +1,136 @@
+package dev.morphia.mapping.codec.pojo;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import com.mongodb.lang.Nullable;
+
+import dev.morphia.annotations.internal.MorphiaInternal;
+import dev.morphia.mapping.codec.EnumCodec;
+import dev.morphia.mapping.codec.MorphiaDateCodec;
+import dev.morphia.mapping.codec.MorphiaLocalDateTimeCodec;
+
+import org.bson.BsonType;
+import org.bson.codecs.BigDecimalCodec;
+import org.bson.codecs.BooleanCodec;
+import org.bson.codecs.Codec;
+import org.bson.codecs.DateCodec;
+import org.bson.codecs.Decimal128Codec;
+import org.bson.codecs.DoubleCodec;
+import org.bson.codecs.IntegerCodec;
+import org.bson.codecs.LongCodec;
+import org.bson.codecs.ObjectIdCodec;
+import org.bson.codecs.StringCodec;
+
+import static java.util.Map.entry;
+
+/**
+ * The per-codec state a {@link MorphiaCodec} reads while encoding and decoding, built once when the codec is created.
+ * Properties are addressed by their {@link PropertyModel#slot() slot}, their position in their entity model, so a
+ * lookup is an array index.
+ *
+ * It holds what belongs to one codec rather than to the shared mapping model: the codecs resolved for this datastore
+ * and the order properties are encoded in. Later work adds more per-slot data here.
+ *
+ *
+ * @hidden
+ * @morphia.internal
+ */
+@MorphiaInternal
+public final class CodecPlan {
+ private final EntityModel model;
+ private final PropertyModel[] properties;
+ private final Codec super Object>[] codecs;
+ private final PropertyModel[] encodeOrder;
+ // per slot, a bitmask of the BSON types (by ordinal) the slot's codec reads without being able to fail part-way
+ private final int[] directBsonTypes;
+
+ /**
+ * Codecs that read the listed BSON types directly: they either read the value and return it, or throw before
+ * reading anything. A property whose codec reads the current type directly can be decoded without a reader mark,
+ * since there's never a partly consumed value to rewind. Keyed on the exact codec class, so subclasses and user
+ * codecs keep the mark. Types a codec reads and then may reject, such as an {@code IntegerCodec} reading an INT64
+ * that doesn't fit, aren't listed.
+ */
+ static final Map, Integer> DIRECT_BSON_TYPES = Map.ofEntries(
+ entry(StringCodec.class, mask(BsonType.STRING, BsonType.SYMBOL)),
+ entry(ObjectIdCodec.class, mask(BsonType.OBJECT_ID)),
+ entry(IntegerCodec.class, mask(BsonType.INT32)),
+ entry(LongCodec.class, mask(BsonType.INT32, BsonType.INT64)),
+ entry(DoubleCodec.class, mask(BsonType.DOUBLE, BsonType.INT32)),
+ entry(BooleanCodec.class, mask(BsonType.BOOLEAN)),
+ entry(Decimal128Codec.class, mask(BsonType.DECIMAL128)),
+ entry(BigDecimalCodec.class, mask(BsonType.DECIMAL128)),
+ entry(DateCodec.class, mask(BsonType.DATE_TIME)),
+ entry(EnumCodec.class, mask(BsonType.STRING)),
+ entry(MorphiaLocalDateTimeCodec.class, mask(BsonType.DATE_TIME, BsonType.INT64)),
+ entry(MorphiaDateCodec.class, mask(BsonType.DATE_TIME)));
+
+ @SuppressWarnings("unchecked")
+ CodecPlan(EntityModel model) {
+ this.model = model;
+ List list = model.getProperties();
+ properties = list.toArray(new PropertyModel[0]);
+ codecs = new Codec[properties.length];
+ directBsonTypes = new int[properties.length];
+ for (int slot = 0; slot < properties.length; slot++) {
+ PropertyModel property = properties[slot];
+ if (property.slot() != slot) {
+ throw new IllegalStateException("%s is in slot %d of %s but records slot %d".formatted(property.getName(), slot,
+ model.getType().getName(), property.slot()));
+ }
+ codecs[slot] = (Codec super Object>) property.getCodec();
+ directBsonTypes[slot] = codecs[slot] != null ? DIRECT_BSON_TYPES.getOrDefault(codecs[slot].getClass(), 0) : 0;
+ }
+ PropertyModel idProperty = model.getIdProperty();
+ encodeOrder = Arrays.stream(properties)
+ .filter(property -> property != idProperty)
+ .toArray(PropertyModel[]::new);
+ }
+
+ /**
+ * @return the entity model this plan is for
+ */
+ public EntityModel model() {
+ return model;
+ }
+
+ /**
+ * @param property a property of this plan's model
+ * @return the codec for the property, or null if none was resolved
+ */
+ @Nullable
+ public Codec super Object> codec(PropertyModel property) {
+ int slot = property.slot();
+ if (slot < 0 || slot >= properties.length || properties[slot] != property) {
+ throw new IllegalArgumentException("%s isn't a property of %s".formatted(property, model.getType().getName()));
+ }
+ return codecs[slot];
+ }
+
+ /**
+ * @param property a property of this plan's model
+ * @param type the BSON type about to be read for it
+ * @return true if the property's codec reads that type without being able to fail part-way, so decoding it needs no
+ * reader mark to fall back to
+ */
+ public boolean readsDirectly(PropertyModel property, BsonType type) {
+ return (directBsonTypes[property.slot()] & (1 << type.ordinal())) != 0;
+ }
+
+ private static int mask(BsonType... types) {
+ int mask = 0;
+ for (BsonType type : types) {
+ mask |= 1 << type.ordinal();
+ }
+ return mask;
+ }
+
+ /**
+ * @return the properties to encode after the id, in order. Callers must not modify the array.
+ */
+ PropertyModel[] encodeOrder() {
+ return encodeOrder;
+ }
+}
diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityDecoder.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityDecoder.java
index 61249f2a9f4..186bd6da66a 100644
--- a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityDecoder.java
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityDecoder.java
@@ -64,7 +64,7 @@ public T decode(BsonReader reader, DecoderContext decoderContext) {
boolean decoded = false;
try {
- decodeProperties(reader, decoderContext, instanceCreator, classModel);
+ decodeProperties(reader, decoderContext, instanceCreator, morphiaCodec.getPlan());
decoded = true;
} finally {
if (prereadId != null) {
@@ -99,35 +99,79 @@ public T decode(BsonReader reader, DecoderContext decoderContext) {
protected void decodeModel(BsonReader reader, DecoderContext decoderContext,
MorphiaInstanceCreator instanceCreator, @Nullable PropertyModel model) {
+ decodeModel(reader, decoderContext, instanceCreator, morphiaCodec.getPlan(), model);
+ }
- if (model != null) {
- final BsonReaderMark mark = reader.getMark();
- try {
- if (reader.getCurrentBsonType() == BsonType.NULL) {
- reader.readNull();
- } else {
- Object value = decoderContext.decodeWithChildContext(model.getCodec(), reader);
- instanceCreator.set(value, model);
- }
- } catch (BsonInvalidOperationException e) {
- mark.reset();
- final Object value = morphiaCodec.getRegistry().get(Object.class).decode(reader, decoderContext);
- instanceCreator.set(morphiaCodec.getConversions().convert(value, model.getTypeData().getType()), model);
- }
- } else {
+ /**
+ * Decodes a property with the codec the given plan holds for it.
+ *
+ * @param reader the reader
+ * @param decoderContext the context
+ * @param instanceCreator the creator to give the value to
+ * @param plan the plan of the codec for the model the property belongs to
+ * @param model the property, or null to skip the value
+ * @hidden
+ * @morphia.internal
+ */
+ @MorphiaInternal
+ protected void decodeModel(BsonReader reader, DecoderContext decoderContext,
+ MorphiaInstanceCreator instanceCreator, CodecPlan plan, @Nullable PropertyModel model) {
+
+ if (model == null) {
reader.skipValue();
+ return;
+ }
+ BsonType type = reader.getCurrentBsonType();
+ if (type == BsonType.NULL) {
+ reader.readNull();
+ return;
+ }
+ if (plan.readsDirectly(model, type)) {
+ // the codec can't fail part-way on this type, so there's nothing to rewind and no mark to take
+ instanceCreator.set(decoderContext.decodeWithChildContext(plan.codec(model), reader), model);
+ return;
+ }
+ final BsonReaderMark mark = reader.getMark();
+ try {
+ Object value = decoderContext.decodeWithChildContext(plan.codec(model), reader);
+ instanceCreator.set(value, model);
+ } catch (BsonInvalidOperationException e) {
+ mark.reset();
+ final Object value = morphiaCodec.getRegistry().get(Object.class).decode(reader, decoderContext);
+ instanceCreator.set(morphiaCodec.getConversions().convert(value, model.getTypeData().getType()), model);
}
}
protected void decodeProperties(BsonReader reader, DecoderContext decoderContext,
MorphiaInstanceCreator instanceCreator, EntityModel classModel) {
+ CodecPlan plan = morphiaCodec.getPlan();
+ if (plan.model() != classModel) {
+ plan = ((MorphiaCodec>) morphiaCodec.getRegistry().get(classModel.getType())).getPlan();
+ }
+ decodeProperties(reader, decoderContext, instanceCreator, plan);
+ }
+
+ /**
+ * Decodes a document's properties with the codecs a plan holds.
+ *
+ * @param reader the reader
+ * @param decoderContext the context
+ * @param instanceCreator the creator to give the values to
+ * @param plan the plan of the codec for the model being decoded
+ * @hidden
+ * @morphia.internal
+ */
+ @MorphiaInternal
+ protected void decodeProperties(BsonReader reader, DecoderContext decoderContext,
+ MorphiaInstanceCreator instanceCreator, CodecPlan plan) {
+ EntityModel classModel = plan.model();
reader.readStartDocument();
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
String name = reader.readName();
if (classModel.useDiscriminator() && classModel.discriminatorKey().equals(name)) {
reader.readString();
} else {
- decodeModel(reader, decoderContext, instanceCreator, classModel.getProperty(name));
+ decodeModel(reader, decoderContext, instanceCreator, plan, classModel.getProperty(name));
}
}
reader.readEndDocument();
diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java
index c4175ee7332..2e8176d7fe2 100644
--- a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java
@@ -8,7 +8,6 @@
import dev.morphia.annotations.internal.MorphiaInternal;
import org.bson.BsonWriter;
-import org.bson.codecs.Codec;
import org.bson.codecs.EncoderContext;
import org.bson.codecs.IdGenerator;
import org.bson.codecs.ObjectIdGenerator;
@@ -52,10 +51,7 @@ public void encode(BsonWriter writer, T value, EncoderContext encoderContext) {
encodeDiscriminator(writer, model);
}
- for (PropertyModel propertyModel : model.properties()) {
- if (propertyModel.equals(idModel)) {
- continue;
- }
+ for (PropertyModel propertyModel : morphiaCodec.getPlan().encodeOrder()) {
encodeProperty(writer, propertyModel, value, encoderContext);
}
});
@@ -138,8 +134,7 @@ protected void writeValue(BsonWriter writer, EncoderContext encoderContext, Prop
if (value == null) {
writer.writeNull();
} else {
- Codec super Object> codec = (Codec super Object>) model.getCodec();
- encoderContext.encodeWithChildContext(codec, writer, value);
+ encoderContext.encodeWithChildContext(morphiaCodec.getPlan().codec(model), writer, value);
}
}
}
diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java
index b913849d031..617d00ca165 100644
--- a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java
@@ -170,6 +170,7 @@ public boolean addProperty(PropertyModel property) {
added &= propertyModelsByMappedName.put(property.getMappedName(), property) == null;
if (added) {
+ property.slot(propertyModelsByName.size() - 1);
if (property.hasAnnotation(Id.class)) {
idProperty = property;
} else if (property.hasAnnotation(Version.class)) {
diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/LifecycleDecoder.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/LifecycleDecoder.java
index 4a84e825c0a..f3f967f459e 100644
--- a/core/src/main/java/dev/morphia/mapping/codec/pojo/LifecycleDecoder.java
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/LifecycleDecoder.java
@@ -37,6 +37,7 @@ public LifecycleDecoder(MorphiaCodec codec) {
public T decode(BsonReader reader, DecoderContext decoderContext) {
Document document = getMorphiaCodec().getRegistry().get(Document.class).decode(reader, decoderContext);
EntityModel model = getMorphiaCodec().getEntityModel();
+ CodecPlan plan = getMorphiaCodec().getPlan();
if (model.useDiscriminator()) {
String discriminator = document.getString(model.discriminatorKey());
if (discriminator != null) {
@@ -45,6 +46,7 @@ public T decode(BsonReader reader, DecoderContext decoderContext) {
Codec> codec = getMorphiaCodec().getRegistry().get(discriminatorClass);
if (codec instanceof MorphiaCodec) {
model = ((MorphiaCodec>) codec).getEntityModel();
+ plan = ((MorphiaCodec>) codec).getPlan();
} else {
throw new CodecConfigurationException(format("Non-entity class used as discriminator: '%s'.", discriminator));
}
@@ -70,7 +72,7 @@ public T decode(BsonReader reader, DecoderContext decoderContext) {
try {
model.callLifecycleMethods(PreLoad.class, entity, document, getMorphiaCodec().getDatastore());
decodeProperties(new DocumentReader(document, getMorphiaCodec().getConversions()), decoderContext, instanceCreator,
- model);
+ plan);
model.callLifecycleMethods(PostLoad.class, entity, document, getMorphiaCodec().getDatastore());
decoded = true;
} finally {
diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/MorphiaCodec.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/MorphiaCodec.java
index 8dd15c04db6..7ddd16bcae8 100644
--- a/core/src/main/java/dev/morphia/mapping/codec/pojo/MorphiaCodec.java
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/MorphiaCodec.java
@@ -47,6 +47,7 @@ public class MorphiaCodec implements CollectibleCodec {
private final PropertyCodecRegistry propertyCodecRegistry;
private final DiscriminatorLookup discriminatorLookup;
private final Conversions conversions;
+ private final CodecPlan plan;
private EntityEncoder encoder;
private EntityDecoder decoder;
private MorphiaDatastore datastore;
@@ -74,6 +75,7 @@ public MorphiaCodec(MorphiaDatastore datastore, EntityModel model,
this.propertyCodecRegistry = new PropertyCodecRegistryImpl(this, registry, propertyCodecProviders);
idProperty = model.getIdProperty();
specializePropertyCodecs();
+ plan = new CodecPlan(model);
}
@Override
@@ -173,6 +175,16 @@ public EntityModel getEntityModel() {
return entityModel;
}
+ /**
+ * @return the plan this codec encodes and decodes with
+ * @hidden
+ * @morphia.internal
+ */
+ @MorphiaInternal
+ public CodecPlan getPlan() {
+ return plan;
+ }
+
/**
* @return the codec registry
*/
diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/PropertyModel.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/PropertyModel.java
index 21411ea3bd2..78e389eaed6 100644
--- a/core/src/main/java/dev/morphia/mapping/codec/pojo/PropertyModel.java
+++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/PropertyModel.java
@@ -67,6 +67,8 @@ public class PropertyModel {
private final List loadNames = new ArrayList<>();
private final EntityModel entityModel;
private Codec super Object> codec;
+ // this property's position in its entity model, which addresses it in a CodecPlan; assigned when it's added
+ private int slot = -1;
private Class> normalizedType;
private Conversions conversions;
@@ -425,4 +427,15 @@ void codec(Codec super Object> codec) {
this.codec = codec;
}
+ /**
+ * @return this property's position in its entity model, or -1 if it hasn't been added to one
+ */
+ int slot() {
+ return slot;
+ }
+
+ void slot(int slot) {
+ this.slot = slot;
+ }
+
}
diff --git a/core/src/test/java/dev/morphia/mapping/codec/pojo/TestDirectBsonTypes.java b/core/src/test/java/dev/morphia/mapping/codec/pojo/TestDirectBsonTypes.java
new file mode 100644
index 00000000000..741734a4145
--- /dev/null
+++ b/core/src/test/java/dev/morphia/mapping/codec/pojo/TestDirectBsonTypes.java
@@ -0,0 +1,152 @@
+package dev.morphia.mapping.codec.pojo;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+
+import dev.morphia.MorphiaDatastore;
+import dev.morphia.config.ManualMorphiaConfig;
+import dev.morphia.mapping.codec.EnumCodec;
+import dev.morphia.mapping.codec.MorphiaDateCodec;
+import dev.morphia.mapping.codec.MorphiaLocalDateTimeCodec;
+
+import org.bson.BsonBoolean;
+import org.bson.BsonDateTime;
+import org.bson.BsonDecimal128;
+import org.bson.BsonDocument;
+import org.bson.BsonDocumentReader;
+import org.bson.BsonDouble;
+import org.bson.BsonInt32;
+import org.bson.BsonInt64;
+import org.bson.BsonObjectId;
+import org.bson.BsonString;
+import org.bson.BsonSymbol;
+import org.bson.BsonType;
+import org.bson.BsonValue;
+import org.bson.codecs.BigDecimalCodec;
+import org.bson.codecs.BooleanCodec;
+import org.bson.codecs.Codec;
+import org.bson.codecs.DateCodec;
+import org.bson.codecs.Decimal128Codec;
+import org.bson.codecs.DecoderContext;
+import org.bson.codecs.DoubleCodec;
+import org.bson.codecs.IntegerCodec;
+import org.bson.codecs.LongCodec;
+import org.bson.codecs.ObjectIdCodec;
+import org.bson.codecs.StringCodec;
+import org.bson.types.Decimal128;
+import org.bson.types.ObjectId;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static java.util.Map.entry;
+
+/**
+ * Pins the assumption behind {@link CodecPlan}'s direct-type table against the codecs the driver and Morphia actually
+ * provide: for every BSON type a codec is listed as reading directly, it reads the whole value without throwing, so
+ * decoding that type needs no reader mark. A driver upgrade that changes one of these codecs fails here.
+ */
+public class TestDirectBsonTypes {
+ // the Java type whose codec is each table entry
+ private static final Map, Class>> JAVA_TYPES = Map.ofEntries(
+ entry(StringCodec.class, String.class),
+ entry(ObjectIdCodec.class, ObjectId.class),
+ entry(IntegerCodec.class, Integer.class),
+ entry(LongCodec.class, Long.class),
+ entry(DoubleCodec.class, Double.class),
+ entry(BooleanCodec.class, Boolean.class),
+ entry(Decimal128Codec.class, Decimal128.class),
+ entry(BigDecimalCodec.class, BigDecimal.class),
+ entry(DateCodec.class, Date.class),
+ entry(EnumCodec.class, Color.class),
+ entry(MorphiaLocalDateTimeCodec.class, LocalDateTime.class),
+ entry(MorphiaDateCodec.class, LocalDate.class));
+
+ // values of each BSON type, including the extremes a codec might reject after reading
+ private static final Map> SAMPLES = Map.ofEntries(
+ entry(BsonType.STRING, List.of(new BsonString(""), new BsonString("text"))),
+ entry(BsonType.SYMBOL, List.of(new BsonSymbol("symbol"))),
+ entry(BsonType.OBJECT_ID, List.of(new BsonObjectId(new ObjectId()))),
+ entry(BsonType.INT32, List.of(new BsonInt32(Integer.MIN_VALUE), new BsonInt32(0), new BsonInt32(Integer.MAX_VALUE))),
+ entry(BsonType.INT64, List.of(new BsonInt64(Long.MIN_VALUE), new BsonInt64(1_700_000_000_000L),
+ new BsonInt64(Long.MAX_VALUE))),
+ entry(BsonType.DOUBLE, List.of(new BsonDouble(1.5), new BsonDouble(-0.0), new BsonDouble(Double.NaN),
+ new BsonDouble(Double.MAX_VALUE))),
+ entry(BsonType.BOOLEAN, List.of(BsonBoolean.TRUE, BsonBoolean.FALSE)),
+ entry(BsonType.DECIMAL128, List.of(new BsonDecimal128(Decimal128.parse("1.5")),
+ new BsonDecimal128(Decimal128.parse("-12345678901234567890.123")))),
+ entry(BsonType.DATE_TIME, List.of(new BsonDateTime(0), new BsonDateTime(1_700_000_000_000L))));
+
+ private static MongoClient client;
+ private static MorphiaDatastore datastore;
+
+ @BeforeAll
+ static void datastore() {
+ // nothing here talks to a server
+ client = MongoClients.create("mongodb://localhost:1");
+ datastore = new MorphiaDatastore(client, ManualMorphiaConfig.configure()
+ .database("direct_bson_types")
+ .applyIndexes(false)
+ .applyCaps(false)
+ .applyDocumentValidations(false));
+ }
+
+ @AfterAll
+ static void close() {
+ client.close();
+ }
+
+ @Test
+ void listedCodecsReadTheirTypesWithoutFailing() {
+ List checked = new ArrayList<>();
+ CodecPlan.DIRECT_BSON_TYPES.forEach((codecClass, mask) -> {
+ Class> javaType = JAVA_TYPES.get(codecClass);
+ Assertions.assertNotNull(javaType, "no Java type to look up " + codecClass.getName() + " with");
+ Codec> codec = datastore.getCodecRegistry().get(javaType);
+ Assertions.assertSame(codecClass, codec.getClass(),
+ "the table lists " + codecClass.getName() + " but " + javaType.getName() + " resolves to " + codec.getClass());
+
+ for (BsonType type : BsonType.values()) {
+ if ((mask & (1 << type.ordinal())) == 0) {
+ continue;
+ }
+ List samples = SAMPLES.get(type);
+ Assertions.assertNotNull(samples, "no samples of " + type);
+ for (BsonValue sample : samples) {
+ BsonValue value = codec instanceof EnumCodec ? new BsonString(Color.RED.name()) : sample;
+ assertReadsWhole(codec, value);
+ checked.add(codecClass.getSimpleName() + "/" + type);
+ }
+ }
+ });
+ Assertions.assertFalse(checked.isEmpty());
+ }
+
+ private static void assertReadsWhole(Codec> codec, BsonValue value) {
+ try (BsonDocumentReader reader = new BsonDocumentReader(new BsonDocument("v", value))) {
+ reader.readStartDocument();
+ reader.readBsonType();
+ reader.readName();
+ try {
+ codec.decode(reader, DecoderContext.builder().build());
+ } catch (RuntimeException e) {
+ Assertions.fail(codec.getClass().getSimpleName() + " should read " + value + " directly", e);
+ }
+ Assertions.assertEquals(BsonType.END_OF_DOCUMENT, reader.readBsonType(),
+ codec.getClass().getSimpleName() + " should consume all of " + value);
+ }
+ }
+
+ enum Color {
+ RED
+ }
+}
diff --git a/core/src/test/java/dev/morphia/test/mapping/codec/harness/CodecHarnessTest.java b/core/src/test/java/dev/morphia/test/mapping/codec/harness/CodecHarnessTest.java
index f333cc40299..6d3f0116004 100644
--- a/core/src/test/java/dev/morphia/test/mapping/codec/harness/CodecHarnessTest.java
+++ b/core/src/test/java/dev/morphia/test/mapping/codec/harness/CodecHarnessTest.java
@@ -30,7 +30,9 @@
import dev.morphia.mapping.DiscriminatorFunction;
import dev.morphia.mapping.MapperType;
import dev.morphia.mapping.codec.MorphiaCodecProvider;
+import dev.morphia.mapping.codec.pojo.CodecPlan;
import dev.morphia.mapping.codec.pojo.EntityModel;
+import dev.morphia.mapping.codec.pojo.MorphiaCodec;
import dev.morphia.mapping.codec.pojo.PropertyModel;
import dev.morphia.mapping.codec.pojo.critter.CritterEntityModel;
import dev.morphia.test.mapping.codec.harness.models.HarnessArrays;
@@ -56,6 +58,7 @@
import org.bson.BsonBinaryWriter;
import org.bson.BsonDocument;
import org.bson.BsonString;
+import org.bson.BsonType;
import org.bson.RawBsonDocument;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.Codec;
@@ -75,9 +78,11 @@
import org.junit.jupiter.api.TestInstance.Lifecycle;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -262,6 +267,31 @@ void concurrentCodecLookupsShareOneCodec() throws Exception {
}
}
+ @Test
+ void codecPlanResolvesEveryPropertyBySlot() {
+ for (MapperType mapper : MapperType.values()) {
+ MorphiaDatastore datastore = datastore(Variant.DEFAULTS, mapper);
+ CodecPlan plan = ((MorphiaCodec>) codec(datastore, HarnessScalars.class)).getPlan();
+ EntityModel model = datastore.getMapper().getEntityModel(HarnessScalars.class);
+
+ assertSame(model, plan.model());
+ for (PropertyModel property : model.getProperties()) {
+ assertNotNull(plan.codec(property), mapper + ": no codec for " + property.getName());
+ }
+ // only types the codec can't fail part-way on skip the reader mark; drifted types keep it for the fallback
+ PropertyModel name = model.getProperty("name");
+ PropertyModel age = model.getProperty("age");
+ assertTrue(plan.readsDirectly(name, BsonType.STRING), mapper + ": StringCodec reads STRING directly");
+ assertFalse(plan.readsDirectly(name, BsonType.INT32), mapper + ": StringCodec rejects INT32");
+ assertTrue(plan.readsDirectly(age, BsonType.INT32), mapper + ": IntegerCodec reads INT32 directly");
+ assertFalse(plan.readsDirectly(age, BsonType.INT64), mapper + ": IntegerCodec may reject an INT64 after reading it");
+
+ PropertyModel foreign = datastore.getMapper().getEntityModel(HarnessRenames.class).getProperties().get(1);
+ assertThrows(IllegalArgumentException.class, () -> plan.codec(foreign),
+ mapper + ": a property of another model should be rejected");
+ }
+ }
+
@Test
void readsLoadOnlyProperties() {
// @LoadOnly values are never written, so the fixtures can't show that they are read
diff --git a/design/mapping-game-plan.md b/design/mapping-game-plan.md
index 2781a8ec920..9d4363415d6 100644
--- a/design/mapping-game-plan.md
+++ b/design/mapping-game-plan.md
@@ -194,7 +194,7 @@ structural item covers are moved to that item (see Phases 4 and 5).
**Precondition:** the minimal codec plan (3.0) and the harness (0.1). Each item is its own PR, with before/after
JMH numbers on both mappers.
-### 3.0 Minimal per-`MorphiaCodec` plan
+### ~~3.0 Minimal per-`MorphiaCodec` plan~~
- **What:** an immutable object built once per `MorphiaCodec`. Initially it holds `PropertyModel[] properties`,
`Codec>[] codecs` by slot, and `PropertyModel[] encodeOrder`. Later items add `directBsonTypes[]`,
`boxedTypes[]`, creator positions, lifecycle flags and discriminator/subtype caches.
@@ -202,7 +202,7 @@ JMH numbers on both mappers.
the shared `PropertyModel`.
- **Scope:** encoders and decoders read codecs from the plan. `PropertyModel.codec` remains for now (removed in 4.1).
-### 3.1 Skip the reader mark for codecs that can't fail part-way (#1, B1)
+### ~~3.1 Skip the reader mark for codecs that can't fail part-way (#1, B1)~~
- Hoist the NULL check above `getMark()`.
- Add a per-slot `directBsonTypes` bitmask from a static table keyed on the exact codec class (the table is in B1).
User codecs keep the mark.