Skip to content

Commit 64c5315

Browse files
committed
#24: FIX: Using query() DSL with a custom value type caused ClassCastException
1 parent 060b9a9 commit 64c5315

File tree

11 files changed

+130
-24
lines changed

11 files changed

+130
-24
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,14 @@ public final class CustomValueTypes {
2121
private CustomValueTypes() {
2222
}
2323

24-
public static Object preconvert(@NonNull JavaField field, Object value) {
24+
public static Object preconvert(@NonNull JavaField field, @NonNull Object value) {
2525
var cvt = field.getCustomValueType();
2626
if (cvt != null) {
27+
if (cvt.columnClass().equals(value.getClass())) {
28+
// Already preconverted
29+
return value;
30+
}
31+
2732
value = createCustomValueTypeConverter(cvt).toColumn(field, value);
2833

2934
Preconditions.checkArgument(cvt.columnClass().isInstance(value),
@@ -33,9 +38,14 @@ public static Object preconvert(@NonNull JavaField field, Object value) {
3338
return value;
3439
}
3540

36-
public static Object postconvert(@NonNull JavaField field, Object value) {
41+
public static Object postconvert(@NonNull JavaField field, @NonNull Object value) {
3742
var cvt = field.getCustomValueType();
3843
if (cvt != null) {
44+
if (field.getRawType().equals(value.getClass())) {
45+
// Already postconverted
46+
return value;
47+
}
48+
3949
value = createCustomValueTypeConverter(cvt).toJava(field, value);
4050
}
4151
return value;

databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -75,47 +75,44 @@ private static FieldValue ofByteArray(@NonNull ByteArray byteArray) {
7575
}
7676

7777
@NonNull
78-
public static FieldValue ofObj(@NonNull Object obj, @NonNull JavaField jf) {
79-
return ofObj(obj, jf.getField().getColumn());
80-
}
78+
public static FieldValue ofObj(@NonNull Object obj, @NonNull JavaField javaField) {
79+
FieldValueType fvt = FieldValueType.forJavaType(obj.getClass(), javaField.getField().getColumn());
80+
Object postconverted = CustomValueTypes.preconvert(javaField, obj);
8181

82-
@NonNull
83-
@SuppressWarnings({"unchecked", "rawtypes"})
84-
private static FieldValue ofObj(@NonNull Object obj, @Nullable Column column) {
85-
switch (FieldValueType.forJavaType(obj.getClass(), column)) {
82+
switch (fvt) {
8683
case STRING -> {
87-
return ofStr(obj.toString());
84+
return ofStr((String) postconverted);
8885
}
8986
case ENUM -> {
90-
return ofStr(((Enum<?>) obj).name());
87+
return ofStr(((Enum<?>) postconverted).name());
9188
}
9289
case INTEGER -> {
93-
return ofNum(((Number) obj).longValue());
90+
return ofNum(((Number) postconverted).longValue());
9491
}
9592
case REAL -> {
96-
return ofReal(((Number) obj).doubleValue());
93+
return ofReal(((Number) postconverted).doubleValue());
9794
}
9895
case BOOLEAN -> {
99-
return ofBool((Boolean) obj);
96+
return ofBool((Boolean) postconverted);
10097
}
10198
case BYTE_ARRAY -> {
102-
return ofByteArray((ByteArray) obj);
99+
return ofByteArray((ByteArray) postconverted);
103100
}
104101
case TIMESTAMP -> {
105-
return ofTimestamp((Instant) obj);
102+
return ofTimestamp((Instant) postconverted);
106103
}
107104
case COMPOSITE -> {
108-
ObjectSchema schema = ObjectSchema.of(obj.getClass());
105+
ObjectSchema schema = ObjectSchema.of(postconverted.getClass());
109106
List<JavaField> flatFields = schema.flattenFields();
110-
Map<String, Object> flattenedObj = schema.flatten(obj);
107+
Map<String, Object> flattenedObj = schema.flatten(postconverted);
111108

112109
List<JavaFieldValue> allFieldValues = flatFields.stream()
113110
.map(jf -> new JavaFieldValue(jf, flattenedObj.get(jf.getName())))
114111
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
115112
if (allFieldValues.size() == 1) {
116113
JavaFieldValue singleValue = allFieldValues.iterator().next();
117114
Preconditions.checkArgument(singleValue.getValue() != null, "Wrappers must have a non-null value inside them");
118-
return ofObj(singleValue.getValue(), column);
115+
return ofObj(singleValue.getValue(), singleValue.getField());
119116
}
120117
return ofTuple(new Tuple(obj, allFieldValues));
121118
}

repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import tech.ydb.yoj.repository.test.sample.model.Team;
2626
import tech.ydb.yoj.repository.test.sample.model.TypeFreak;
2727
import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry;
28+
import tech.ydb.yoj.repository.test.sample.model.VersionedEntity;
2829

2930
import java.util.Set;
3031

@@ -112,6 +113,11 @@ public Table<UpdateFeedEntry> updateFeedEntries() {
112113
public Table<NetworkAppliance> networkAppliances() {
113114
return table(NetworkAppliance.class);
114115
}
116+
117+
@Override
118+
public Table<VersionedEntity> versionedEntities() {
119+
return table(VersionedEntity.class);
120+
}
115121
}
116122

117123
private static class Supabubble2InMemoryTable extends InMemoryTable<Supabubble2> implements TestEntityOperations.Supabubble2Table {

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import tech.ydb.yoj.repository.test.sample.model.TypeFreak.B;
5656
import tech.ydb.yoj.repository.test.sample.model.TypeFreak.Embedded;
5757
import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry;
58+
import tech.ydb.yoj.repository.test.sample.model.Version;
59+
import tech.ydb.yoj.repository.test.sample.model.VersionedEntity;
5860
import tech.ydb.yoj.repository.test.sample.model.WithUnflattenableField;
5961

6062
import java.time.Instant;
@@ -2701,6 +2703,28 @@ public void customValueType() {
27012703
assertThat(db.tx(() -> db.networkAppliances().find(app1.id()))).isEqualTo(app1);
27022704
}
27032705

2706+
@Test
2707+
public void customValueTypeInFilter() {
2708+
var ve = new VersionedEntity(new VersionedEntity.Id("heyhey", new Version(100L)), new Version(100_500L));
2709+
db.tx(() -> db.versionedEntities().insert(ve));
2710+
assertThat(db.tx(() -> db.versionedEntities().find(ve.id()))).isEqualTo(ve);
2711+
assertThat(db.tx(() -> db.versionedEntities().query()
2712+
.where("id.version").eq(ve.id().version())
2713+
.and("version2").eq(ve.version2())
2714+
.findOne()
2715+
)).isEqualTo(ve);
2716+
assertThat(db.tx(() -> db.versionedEntities().query()
2717+
.where("id.version").eq(100L)
2718+
.and("version2").eq(100_500L)
2719+
.findOne()
2720+
)).isEqualTo(ve);
2721+
assertThat(db.tx(() -> db.versionedEntities().query()
2722+
.where("id.version").eq(100L)
2723+
.and("version2").eq(null)
2724+
.findOne()
2725+
)).isNull();
2726+
}
2727+
27042728
protected void runInTx(Consumer<RepositoryTransaction> action) {
27052729
// We do not retry transactions, because we do not expect conflicts in our test scenarios.
27062730
RepositoryTransaction transaction = startTransaction();

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package tech.ydb.yoj.repository.test.entity;
22

33
import lombok.NonNull;
4-
import tech.ydb.yoj.databind.FieldValueType;
54
import tech.ydb.yoj.repository.db.Entity;
65
import tech.ydb.yoj.repository.db.Repository;
76
import tech.ydb.yoj.repository.test.sample.model.Book;
@@ -21,6 +20,7 @@
2120
import tech.ydb.yoj.repository.test.sample.model.Team;
2221
import tech.ydb.yoj.repository.test.sample.model.TypeFreak;
2322
import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry;
23+
import tech.ydb.yoj.repository.test.sample.model.VersionedEntity;
2424
import tech.ydb.yoj.repository.test.sample.model.WithUnflattenableField;
2525

2626
import java.util.List;
@@ -43,14 +43,12 @@ private TestEntities() {
4343
NonDeserializableEntity.class,
4444
WithUnflattenableField.class,
4545
UpdateFeedEntry.class,
46-
NetworkAppliance.class
46+
NetworkAppliance.class,
47+
VersionedEntity.class
4748
);
4849

4950
@SuppressWarnings("unchecked")
5051
public static Repository init(@NonNull Repository repository) {
51-
FieldValueType.registerStringValueType(TypeFreak.Ticket.class);
52-
FieldValueType.registerStringValueType(TypeFreak.StringValueWrapper.class);
53-
5452
repository.createTablespace();
5553
ALL.forEach(entityClass -> repository.schema(entityClass).create());
5654

repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import tech.ydb.yoj.repository.test.sample.model.Team;
2222
import tech.ydb.yoj.repository.test.sample.model.TypeFreak;
2323
import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry;
24+
import tech.ydb.yoj.repository.test.sample.model.VersionedEntity;
2425

2526
import java.util.ArrayList;
2627
import java.util.Collection;
@@ -63,6 +64,8 @@ default Table<BytePkEntity> bytePkEntities() {
6364

6465
Table<NetworkAppliance> networkAppliances();
6566

67+
Table<VersionedEntity> versionedEntities();
68+
6669
class ProjectTable extends AbstractDelegatingTable<Project> {
6770
public ProjectTable(Table<Project> target) {
6871
super(target);

repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/TypeFreak.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
import lombok.NonNull;
55
import lombok.Value;
66
import lombok.With;
7+
import tech.ydb.yoj.databind.CustomValueType;
78
import tech.ydb.yoj.databind.DbType;
9+
import tech.ydb.yoj.databind.FieldValueType;
10+
import tech.ydb.yoj.databind.converter.StringValueConverter;
811
import tech.ydb.yoj.databind.schema.Column;
912
import tech.ydb.yoj.repository.db.Entity;
1013
import tech.ydb.yoj.repository.db.Table;
@@ -115,6 +118,7 @@ public static class StringView implements Table.ViewId<TypeFreak> {
115118
String stringEmbedded;
116119
}
117120

121+
@CustomValueType(columnValueType = FieldValueType.STRING, columnClass = String.class, converter = StringValueConverter.class)
118122
public static final class StringValueWrapper {
119123
private final String value;
120124

@@ -143,6 +147,7 @@ public String toString() {
143147
* instance method.
144148
* <p><em>E.g.,</em> {@code "XYZ-100500" <=> new Ticket(queue="XYZ", num=100500)}
145149
*/
150+
@CustomValueType(columnValueType = FieldValueType.STRING, columnClass = String.class, converter = StringValueConverter.class)
146151
public record Ticket(@NonNull String queue, int num) {
147152
public Ticket {
148153
Preconditions.checkArgument(num >= 1, "ticket number must be >= 1");
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package tech.ydb.yoj.repository.test.sample.model;
2+
3+
import lombok.NonNull;
4+
import tech.ydb.yoj.databind.converter.ValueConverter;
5+
import tech.ydb.yoj.databind.schema.Schema.JavaField;
6+
7+
public record Version(long value) {
8+
public static final class Converter implements ValueConverter<Version, Long> {
9+
@Override
10+
public @NonNull Long toColumn(@NonNull JavaField field, @NonNull Version v) {
11+
return v.value();
12+
}
13+
14+
@Override
15+
public @NonNull Version toJava(@NonNull JavaField field, @NonNull Long value) {
16+
return new Version(value);
17+
}
18+
}
19+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package tech.ydb.yoj.repository.test.sample.model;
2+
3+
import tech.ydb.yoj.databind.CustomValueType;
4+
import tech.ydb.yoj.databind.FieldValueType;
5+
import tech.ydb.yoj.databind.schema.Column;
6+
import tech.ydb.yoj.repository.db.Entity;
7+
import tech.ydb.yoj.repository.db.RecordEntity;
8+
9+
public record VersionedEntity(
10+
Id id,
11+
@Column(
12+
customValueType = @CustomValueType(
13+
columnValueType = FieldValueType.INTEGER,
14+
columnClass = Long.class,
15+
converter = Version.Converter.class
16+
)
17+
)
18+
Version version2
19+
) implements RecordEntity<VersionedEntity> {
20+
public record Id(
21+
String value,
22+
@Column(
23+
customValueType = @CustomValueType(
24+
columnValueType = FieldValueType.INTEGER,
25+
columnClass = Long.class,
26+
converter = Version.Converter.class
27+
)
28+
)
29+
Version version
30+
) implements Entity.Id<VersionedEntity> {
31+
}
32+
}

repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import tech.ydb.yoj.repository.test.sample.model.Team;
3131
import tech.ydb.yoj.repository.test.sample.model.TypeFreak;
3232
import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry;
33+
import tech.ydb.yoj.repository.test.sample.model.VersionedEntity;
3334
import tech.ydb.yoj.repository.ydb.table.YdbTable;
3435
import tech.ydb.yoj.repository.ydb.yql.YqlPredicate;
3536

@@ -132,6 +133,11 @@ public Table<UpdateFeedEntry> updateFeedEntries() {
132133
public Table<NetworkAppliance> networkAppliances() {
133134
return table(NetworkAppliance.class);
134135
}
136+
137+
@Override
138+
public Table<VersionedEntity> versionedEntities() {
139+
return table(VersionedEntity.class);
140+
}
135141
}
136142

137143
private static class YdbSupabubble2Table extends YdbTable<Supabubble2> implements Supabubble2Table {

repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import tech.ydb.yoj.repository.test.sample.model.Team;
2929
import tech.ydb.yoj.repository.test.sample.model.TypeFreak;
3030
import tech.ydb.yoj.repository.test.sample.model.UpdateFeedEntry;
31+
import tech.ydb.yoj.repository.test.sample.model.VersionedEntity;
3132
import tech.ydb.yoj.repository.ydb.table.YdbTable;
3233

3334
import java.util.List;
@@ -131,6 +132,11 @@ public Table<UpdateFeedEntry> updateFeedEntries() {
131132
public Table<NetworkAppliance> networkAppliances() {
132133
return table(NetworkAppliance.class);
133134
}
135+
136+
@Override
137+
public Table<VersionedEntity> versionedEntities() {
138+
return table(VersionedEntity.class);
139+
}
134140
}
135141

136142
private static class YdbSupabubble2Table extends YdbTable<Supabubble2> implements TestEntityOperations.Supabubble2Table {

0 commit comments

Comments
 (0)