Skip to content

Commit 7cdcce4

Browse files
committed
noticket: FIX: Unexpected NPE when trying to validate conversion result in CustomValueTypes.preconvert()
This happens when a custom-value converter erroneously returns `null` from `ValueConverter.toColumn()`, e.g. because of a bad `null`-returning `toString()` implementation in a string-value type. This should not happen in `CustomValueTypes.postconvert()` because the `null -> null` logic is applied before that, and a null `value` should not get into `CustomValueTypes.postconvert()` **at all**.
1 parent 330322e commit 7cdcce4

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ public static Object preconvert(@NonNull JavaField field, @NonNull Object value)
3737

3838
value = cvt.toColumn(field, value);
3939
Preconditions.checkArgument(cvt.getColumnClass().isInstance(value),
40-
"Custom value type converter %s must produce a non-null value of type columnClass()=%s but got value of type %s",
40+
"Custom value type converter %s must produce a non-null value of type columnClass()=%s but got a %s",
4141
cvt.getConverter().getClass().getCanonicalName(),
4242
cvt.getColumnClass().getCanonicalName(),
43-
value.getClass().getCanonicalName());
43+
// NB: do not simplify this condition to `false`! This is explicitly to catch custom value converters that return a `null` by mistake!
44+
value == null ? "null value" : "value of type " + value.getClass().getCanonicalName());
4445
return value;
4546
}
4647

@@ -50,7 +51,10 @@ public static <C extends Comparable<? super C>> Object postconvert(@NonNull Java
5051
return value;
5152
}
5253

53-
Preconditions.checkArgument(value instanceof Comparable, "postconvert() only takes Comparable values, but got value of %s", value.getClass());
54+
Preconditions.checkArgument(value instanceof Comparable, "postconvert() only takes Comparable values, but got value of %s",
55+
// NB: here we don't need to check `value` for `null`, because it got here from YDB/InMemory repository deserialization logic,
56+
// which won't even call the `postconvert()` method if `value` is `null`
57+
value.getClass().getCanonicalName());
5458

5559
@SuppressWarnings("unchecked") C comparable = (C) value;
5660
return cvt.toJava(field, comparable);

repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
import tech.ydb.yoj.repository.test.sample.TestDb;
4444
import tech.ydb.yoj.repository.test.sample.TestDbImpl;
4545
import tech.ydb.yoj.repository.test.sample.TestEntityOperations;
46+
import tech.ydb.yoj.repository.test.sample.model.BadlyWrappedEntity;
47+
import tech.ydb.yoj.repository.test.sample.model.BadlyWrappedEntity.BadStringValueWrapper;
4648
import tech.ydb.yoj.repository.test.sample.model.Book;
4749
import tech.ydb.yoj.repository.test.sample.model.Bubble;
4850
import tech.ydb.yoj.repository.test.sample.model.BytePkEntity;
@@ -2963,6 +2965,21 @@ public void multiWrapperEntity2WithCustomConverterFullyWrapped() {
29632965
)).isEmpty();
29642966
}
29652967

2968+
@Test
2969+
public void nullWrappingStringValueColumn() {
2970+
// must not be able to save a string-value field that returns null from toString()
2971+
var badWrapperWithNullInside = new BadlyWrappedEntity(new BadlyWrappedEntity.Id("xxx"), new BadStringValueWrapper(null));
2972+
assertThatExceptionOfType(ConversionException.class)
2973+
.isThrownBy(() -> db.tx(() -> db.table(BadlyWrappedEntity.class).save(badWrapperWithNullInside)))
2974+
.withCauseInstanceOf(IllegalArgumentException.class);
2975+
2976+
// null column value -> null string-value type field, without invoking the converter at all
2977+
var noWrapper = new BadlyWrappedEntity(new BadlyWrappedEntity.Id("yyy"), null);
2978+
db.tx(() -> db.table(BadlyWrappedEntity.class).save(noWrapper));
2979+
assertThat(db.tx(() -> db.table(BadlyWrappedEntity.class).find(new BadlyWrappedEntity.Id("yyy"))).badWrapper()).isNull();
2980+
assertThat(db.tx(() -> db.table(BadlyWrappedEntity.class).find(new BadlyWrappedEntity.Id("yyy")))).isEqualTo(noWrapper);
2981+
}
2982+
29662983
@Test
29672984
public void lenientEnumBehaviorExplicit() {
29682985
try {

repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import tech.ydb.yoj.repository.db.Entity;
55
import tech.ydb.yoj.repository.db.Repository;
66
import tech.ydb.yoj.repository.db.TableDescriptor;
7+
import tech.ydb.yoj.repository.test.sample.model.BadlyWrappedEntity;
78
import tech.ydb.yoj.repository.test.sample.model.Book;
89
import tech.ydb.yoj.repository.test.sample.model.Bubble;
910
import tech.ydb.yoj.repository.test.sample.model.BytePkEntity;
@@ -62,6 +63,7 @@ private TestEntities() {
6263
DetachedEntity.class,
6364
MultiWrappedEntity.class,
6465
MultiWrappedEntity2.class,
66+
BadlyWrappedEntity.class,
6567
UniqueEntity.class,
6668
UniqueEntityNative.class,
6769
EnumEntity.class
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package tech.ydb.yoj.repository.test.sample.model;
2+
3+
import lombok.NonNull;
4+
import lombok.RequiredArgsConstructor;
5+
import tech.ydb.yoj.databind.converter.StringValueType;
6+
import tech.ydb.yoj.repository.db.Entity;
7+
import tech.ydb.yoj.repository.db.RecordEntity;
8+
9+
import javax.annotation.Nullable;
10+
11+
public record BadlyWrappedEntity(
12+
@NonNull Id id,
13+
@Nullable BadStringValueWrapper badWrapper
14+
) implements RecordEntity<BadlyWrappedEntity> {
15+
public record Id(String value) implements Entity.Id<BadlyWrappedEntity> {
16+
}
17+
18+
@StringValueType
19+
@RequiredArgsConstructor
20+
public static final class BadStringValueWrapper {
21+
private final String value;
22+
23+
@NonNull
24+
public static BadStringValueWrapper valueOf(@NonNull String str) {
25+
return new BadStringValueWrapper(str);
26+
}
27+
28+
@Override
29+
public String toString() {
30+
// MWAHAHAHAHAHAH!!! StringValueConverter doesn't expect that, for sure!
31+
return value;
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)