diff --git a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java new file mode 100644 index 00000000..a296188e --- /dev/null +++ b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java @@ -0,0 +1,30 @@ +package tech.ydb.yoj.databind; + +import tech.ydb.yoj.ExperimentalApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target(TYPE) +@Retention(RUNTIME) +@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") +public @interface CustomValueType { + /** + * Simple value type that the {@link #converter()} represents a custom value type as. + * Cannot be {@link FieldValueType#COMPOSITE} or {@link FieldValueType#UNKNOWN}. + */ + FieldValueType columnValueType(); + + /** + * Type of value that {@link #converter() converter's} {@link ValueConverter#toColumn(Object) toColumn()} method returns + */ + Class columnClass(); + + /** + * Converter class. Must have a no-args public constructor. + */ + Class> converter(); +} diff --git a/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java b/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java index 7782c877..bdb206ad 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java @@ -5,7 +5,6 @@ import org.jetbrains.annotations.NotNull; import tech.ydb.yoj.ExperimentalApi; import tech.ydb.yoj.databind.schema.Column; -import tech.ydb.yoj.databind.schema.StringValueType; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -161,6 +160,11 @@ public static FieldValueType forJavaType(@NonNull Type type) { if (type instanceof ParameterizedType || type instanceof TypeVariable) { return OBJECT; } else if (type instanceof Class clazz) { + var cvt = clazz.getAnnotation(CustomValueType.class); + if (cvt != null) { + return cvt.columnValueType(); + } + if (isStringValueType(clazz)) { return STRING; } else if (INTEGER_NUMERIC_TYPES.contains(clazz)) { diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/StringValueType.java b/databind/src/main/java/tech/ydb/yoj/databind/StringValueType.java similarity index 95% rename from databind/src/main/java/tech/ydb/yoj/databind/schema/StringValueType.java rename to databind/src/main/java/tech/ydb/yoj/databind/StringValueType.java index e492dbf0..6e239bef 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/StringValueType.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/StringValueType.java @@ -1,4 +1,4 @@ -package tech.ydb.yoj.databind.schema; +package tech.ydb.yoj.databind; import tech.ydb.yoj.ExperimentalApi; diff --git a/databind/src/main/java/tech/ydb/yoj/databind/ValueConverter.java b/databind/src/main/java/tech/ydb/yoj/databind/ValueConverter.java new file mode 100644 index 00000000..aef8c695 --- /dev/null +++ b/databind/src/main/java/tech/ydb/yoj/databind/ValueConverter.java @@ -0,0 +1,19 @@ +package tech.ydb.yoj.databind; + +import lombok.NonNull; +import tech.ydb.yoj.ExperimentalApi; + +/** + * Custom value conversion logic. Must have a no-args public constructor. + * + * @param Java value type + * @param Database column value type + */ +@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") +public interface ValueConverter { + @NonNull + C toColumn(@NonNull V v); + + @NonNull + V toJava(@NonNull C c); +} diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/CustomConverterException.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/CustomConverterException.java new file mode 100644 index 00000000..7a0c4fa5 --- /dev/null +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/CustomConverterException.java @@ -0,0 +1,11 @@ +package tech.ydb.yoj.databind.schema; + +import org.jetbrains.annotations.Nullable; +import tech.ydb.yoj.ExperimentalApi; + +@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") +public final class CustomConverterException extends BindingException { + public CustomConverterException(@Nullable Throwable cause, String message) { + super(cause, __ -> message); + } +} diff --git a/databind/src/main/java/tech/ydb/yoj/databind/schema/Schema.java b/databind/src/main/java/tech/ydb/yoj/databind/schema/Schema.java index e1a7f722..3a171867 100644 --- a/databind/src/main/java/tech/ydb/yoj/databind/schema/Schema.java +++ b/databind/src/main/java/tech/ydb/yoj/databind/schema/Schema.java @@ -8,7 +8,10 @@ import lombok.SneakyThrows; import lombok.Value; import lombok.With; +import tech.ydb.yoj.ExperimentalApi; +import tech.ydb.yoj.databind.CustomValueType; import tech.ydb.yoj.databind.FieldValueType; +import tech.ydb.yoj.databind.StringValueType; import tech.ydb.yoj.databind.schema.configuration.SchemaRegistry.SchemaKey; import tech.ydb.yoj.databind.schema.naming.NamingStrategy; import tech.ydb.yoj.databind.schema.reflect.ReflectField; @@ -695,6 +698,12 @@ private JavaField findField(List path) { .orElse(null); } + @Nullable + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") + public CustomValueType getCustomValueType() { + return field.getType().getAnnotation(CustomValueType.class); + } + @Override public String toString() { return getType().getTypeName() + " " + field.getName(); diff --git a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/Columns.java b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/Columns.java index 3405ab29..e0206276 100644 --- a/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/Columns.java +++ b/repository-inmemory/src/main/java/tech/ydb/yoj/repository/test/inmemory/Columns.java @@ -71,6 +71,9 @@ private static Object serialize(Schema.JavaField field, Object value) { String qualifier = field.getDbTypeQualifier(); try { Preconditions.checkState(field.isSimple(), "Trying to serialize a non-simple field: %s", field); + + value = CommonConverters.preconvert(field.getCustomValueType(), value); + return switch (field.getValueType()) { case STRING -> CommonConverters.serializeStringValue(type, value); case ENUM -> DbTypeQualifier.ENUM_TO_STRING.equals(qualifier) @@ -97,7 +100,8 @@ private static Object deserialize(Schema.JavaField field, Object value) { String qualifier = field.getDbTypeQualifier(); try { Preconditions.checkState(field.isSimple(), "Trying to deserialize a non-simple field: %s", field); - return switch (field.getValueType()) { + + var deserialized = switch (field.getValueType()) { case STRING -> CommonConverters.deserializeStringValue(type, value); case ENUM -> DbTypeQualifier.ENUM_TO_STRING.equals(qualifier) ? CommonConverters.deserializeEnumToStringValue(type, value) @@ -110,6 +114,8 @@ private static Object deserialize(Schema.JavaField field, Object value) { case INTERVAL, TIMESTAMP -> value; default -> throw new IllegalStateException("Don't know how to deserialize field: " + field); }; + + return CommonConverters.postconvert(field.getCustomValueType(), deserialized); } catch (Exception e) { throw new ConversionException("Could not deserialize value of type <" + type + ">", e); } diff --git a/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java b/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java index e892b6c6..4af28131 100644 --- a/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java +++ b/repository-inmemory/src/test/java/tech/ydb/yoj/repository/test/inmemory/TestInMemoryRepository.java @@ -16,6 +16,7 @@ import tech.ydb.yoj.repository.test.sample.model.EntityWithValidation; import tech.ydb.yoj.repository.test.sample.model.IndexedEntity; import tech.ydb.yoj.repository.test.sample.model.LogEntry; +import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance; import tech.ydb.yoj.repository.test.sample.model.Primitive; import tech.ydb.yoj.repository.test.sample.model.Project; import tech.ydb.yoj.repository.test.sample.model.Referring; @@ -106,6 +107,11 @@ public Supabubble2Table supabubbles2() { public Table updateFeedEntries() { return table(UpdateFeedEntry.class); } + + @Override + public Table networkAppliances() { + return table(NetworkAppliance.class); + } } private static class Supabubble2InMemoryTable extends InMemoryTable implements TestEntityOperations.Supabubble2Table { diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java index 9d3517ef..c2df63e4 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/RepositoryTest.java @@ -2,6 +2,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterators; +import lombok.SneakyThrows; import org.assertj.core.api.Assertions; import org.junit.Assert; import org.junit.Test; @@ -40,6 +41,7 @@ import tech.ydb.yoj.repository.test.sample.model.EntityWithValidation; import tech.ydb.yoj.repository.test.sample.model.IndexedEntity; import tech.ydb.yoj.repository.test.sample.model.MultiLevelDirectory; +import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance; import tech.ydb.yoj.repository.test.sample.model.NonDeserializableEntity; import tech.ydb.yoj.repository.test.sample.model.NonDeserializableObject; import tech.ydb.yoj.repository.test.sample.model.Primitive; @@ -2632,6 +2634,14 @@ public void stringValuedIdInsert() { } } + @Test + @SneakyThrows + public void customValueType() { + var app1 = new NetworkAppliance(new NetworkAppliance.Id("app1"), new NetworkAppliance.Ipv6Address("2e:a0::1")); + db.tx(() -> db.networkAppliances().insert(app1)); + assertThat(db.tx(() -> db.networkAppliances().find(app1.id()))).isEqualTo(app1); + } + protected void runInTx(Consumer action) { // We do not retry transactions, because we do not expect conflicts in our test scenarios. RepositoryTransaction transaction = startTransaction(); diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java index 5de0fc37..8f5a7429 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/entity/TestEntities.java @@ -11,6 +11,7 @@ import tech.ydb.yoj.repository.test.sample.model.EntityWithValidation; import tech.ydb.yoj.repository.test.sample.model.IndexedEntity; import tech.ydb.yoj.repository.test.sample.model.LogEntry; +import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance; import tech.ydb.yoj.repository.test.sample.model.NonDeserializableEntity; import tech.ydb.yoj.repository.test.sample.model.Primitive; import tech.ydb.yoj.repository.test.sample.model.Project; @@ -41,7 +42,8 @@ private TestEntities() { Supabubble2.class, NonDeserializableEntity.class, WithUnflattenableField.class, - UpdateFeedEntry.class + UpdateFeedEntry.class, + NetworkAppliance.class ); @SuppressWarnings("unchecked") diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java index b27fff6a..1ce2ae81 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/TestEntityOperations.java @@ -12,6 +12,7 @@ import tech.ydb.yoj.repository.test.sample.model.EntityWithValidation; import tech.ydb.yoj.repository.test.sample.model.IndexedEntity; import tech.ydb.yoj.repository.test.sample.model.LogEntry; +import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance; import tech.ydb.yoj.repository.test.sample.model.Primitive; import tech.ydb.yoj.repository.test.sample.model.Project; import tech.ydb.yoj.repository.test.sample.model.Referring; @@ -60,6 +61,8 @@ default Table bytePkEntities() { Table updateFeedEntries(); + Table networkAppliances(); + class ProjectTable extends AbstractDelegatingTable { public ProjectTable(Table target) { super(target); diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/NetworkAppliance.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/NetworkAppliance.java new file mode 100644 index 00000000..57ede5e9 --- /dev/null +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/NetworkAppliance.java @@ -0,0 +1,50 @@ +package tech.ydb.yoj.repository.test.sample.model; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import tech.ydb.yoj.databind.CustomValueType; +import tech.ydb.yoj.databind.ValueConverter; +import tech.ydb.yoj.repository.db.RecordEntity; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import static tech.ydb.yoj.databind.FieldValueType.BINARY; + +public record NetworkAppliance( + @NonNull Id id, + @NonNull Ipv6Address address +) implements RecordEntity { + public record Id(@NonNull String value) implements RecordEntity.Id { + } + + @CustomValueType(columnValueType = BINARY, columnClass = byte[].class, converter = Ipv6Address.Converter.class) + @Value + @RequiredArgsConstructor + public static class Ipv6Address { + Inet6Address addr; + + public Ipv6Address(String ip6) throws UnknownHostException { + this((Inet6Address) InetAddress.getByName(ip6)); + } + + public static final class Converter implements ValueConverter { + @Override + public byte @NonNull [] toColumn(@NonNull Ipv6Address ipv6Address) { + return ipv6Address.addr.getAddress(); + } + + @NonNull + @Override + public Ipv6Address toJava(byte @NonNull [] bytes) { + try { + return new Ipv6Address((Inet6Address) InetAddress.getByAddress(bytes)); + } catch (UnknownHostException neverHappens) { + throw new InternalError(neverHappens); + } + } + } + } +} diff --git a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/UpdateFeedEntry.java b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/UpdateFeedEntry.java index 71c35cca..76ed8981 100644 --- a/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/UpdateFeedEntry.java +++ b/repository-test/src/main/java/tech/ydb/yoj/repository/test/sample/model/UpdateFeedEntry.java @@ -3,8 +3,8 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.Value; +import tech.ydb.yoj.databind.StringValueType; import tech.ydb.yoj.databind.schema.Column; -import tech.ydb.yoj.databind.schema.StringValueType; import tech.ydb.yoj.repository.DbTypeQualifier; import tech.ydb.yoj.repository.db.Entity; diff --git a/repository-ydb-v1/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java b/repository-ydb-v1/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java index 85a580cf..6674c2af 100644 --- a/repository-ydb-v1/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java +++ b/repository-ydb-v1/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java @@ -1,6 +1,7 @@ package tech.ydb.yoj.repository.ydb.yql; import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import com.google.protobuf.UnsafeByteOperations; @@ -13,6 +14,7 @@ import lombok.Value; import lombok.With; import tech.ydb.yoj.databind.ByteArray; +import tech.ydb.yoj.databind.CustomValueType; import tech.ydb.yoj.databind.FieldValueType; import tech.ydb.yoj.databind.schema.Column; import tech.ydb.yoj.databind.schema.Schema.JavaField; @@ -40,6 +42,8 @@ import static tech.ydb.yoj.repository.db.common.CommonConverters.enumValueSetter; import static tech.ydb.yoj.repository.db.common.CommonConverters.opaqueObjectValueGetter; import static tech.ydb.yoj.repository.db.common.CommonConverters.opaqueObjectValueSetter; +import static tech.ydb.yoj.repository.db.common.CommonConverters.postconvert; +import static tech.ydb.yoj.repository.db.common.CommonConverters.preconvert; import static tech.ydb.yoj.repository.db.common.CommonConverters.stringValueGetter; import static tech.ydb.yoj.repository.db.common.CommonConverters.stringValueSetter; @@ -332,7 +336,9 @@ public static void resetStringDefaultTypeToDefaults() { @NonNull public static YqlPrimitiveType of(Type javaType) { - return resolveYqlType(javaType, FieldValueType.forJavaType(javaType), null, null); + var cvt = TypeToken.of(javaType).getRawType().getAnnotation(CustomValueType.class); + var valueType = FieldValueType.forJavaType(javaType); + return resolveYqlType(javaType, valueType, null, null, cvt); } /** @@ -348,7 +354,34 @@ public static YqlPrimitiveType of(JavaField column) { String columnType = column.getDbType(); PrimitiveTypeId yqlType = (columnType == null) ? null : convertToYqlType(columnType); - return resolveYqlType(column.getType(), column.getValueType(), yqlType, column.getDbTypeQualifier()); + return resolveYqlType(column.getType(), column.getValueType(), yqlType, column.getDbTypeQualifier(), column.getCustomValueType()); + } + + @NonNull + private static YqlPrimitiveType resolveYqlType(Type javaType, FieldValueType valueType, + PrimitiveTypeId yqlType, String qualifier, + CustomValueType cvt) { + if (cvt != null && cvt.columnValueType() != valueType) { + throw new IllegalStateException("This should never happen: detected FieldValueType must == @CustomValueType.columnValueType(), but got: " + + valueType + " != " + cvt.columnValueType()); + } + + var underlyingType = resolveYqlType( + cvt != null ? cvt.columnClass() : javaType, + valueType, + yqlType, + qualifier + ); + if (cvt == null) { + return underlyingType; + } + + return new YqlPrimitiveType( + underlyingType.javaType, + underlyingType.yqlType, + (b, o) -> underlyingType.setter.accept(b, preconvert(cvt, o)), + v -> postconvert(cvt, underlyingType.getter.apply(v)) + ); } @NonNull diff --git a/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java b/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java index d4e718bc..5bd7df15 100644 --- a/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java +++ b/repository-ydb-v1/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java @@ -21,6 +21,7 @@ import tech.ydb.yoj.repository.test.sample.model.EntityWithValidation; import tech.ydb.yoj.repository.test.sample.model.IndexedEntity; import tech.ydb.yoj.repository.test.sample.model.LogEntry; +import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance; import tech.ydb.yoj.repository.test.sample.model.Primitive; import tech.ydb.yoj.repository.test.sample.model.Project; import tech.ydb.yoj.repository.test.sample.model.Referring; @@ -126,6 +127,11 @@ public Supabubble2Table supabubbles2() { public Table updateFeedEntries() { return table(UpdateFeedEntry.class); } + + @Override + public Table networkAppliances() { + return table(NetworkAppliance.class); + } } private static class YdbSupabubble2Table extends YdbTable implements Supabubble2Table { diff --git a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java index e9fd567c..f5ad7f18 100644 --- a/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java +++ b/repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlPrimitiveType.java @@ -1,6 +1,7 @@ package tech.ydb.yoj.repository.ydb.yql; import com.google.common.primitives.Primitives; +import com.google.common.reflect.TypeToken; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors; import com.google.protobuf.UnsafeByteOperations; @@ -13,6 +14,7 @@ import tech.ydb.proto.ValueProtos.Value.ValueCase; import tech.ydb.table.values.proto.ProtoValue; import tech.ydb.yoj.databind.ByteArray; +import tech.ydb.yoj.databind.CustomValueType; import tech.ydb.yoj.databind.FieldValueType; import tech.ydb.yoj.databind.schema.Column; import tech.ydb.yoj.databind.schema.Schema.JavaField; @@ -40,6 +42,8 @@ import static tech.ydb.yoj.repository.db.common.CommonConverters.enumValueSetter; import static tech.ydb.yoj.repository.db.common.CommonConverters.opaqueObjectValueGetter; import static tech.ydb.yoj.repository.db.common.CommonConverters.opaqueObjectValueSetter; +import static tech.ydb.yoj.repository.db.common.CommonConverters.postconvert; +import static tech.ydb.yoj.repository.db.common.CommonConverters.preconvert; import static tech.ydb.yoj.repository.db.common.CommonConverters.stringValueGetter; import static tech.ydb.yoj.repository.db.common.CommonConverters.stringValueSetter; @@ -332,7 +336,9 @@ public static void resetStringDefaultTypeToDefaults() { @NonNull public static YqlPrimitiveType of(Type javaType) { - return resolveYqlType(javaType, FieldValueType.forJavaType(javaType), null, null); + var cvt = TypeToken.of(javaType).getRawType().getAnnotation(CustomValueType.class); + var valueType = FieldValueType.forJavaType(javaType); + return resolveYqlType(javaType, valueType, null, null, cvt); } /** @@ -348,7 +354,34 @@ public static YqlPrimitiveType of(JavaField column) { String columnType = column.getDbType(); PrimitiveTypeId yqlType = (columnType == null) ? null : convertToYqlType(columnType); - return resolveYqlType(column.getType(), column.getValueType(), yqlType, column.getDbTypeQualifier()); + return resolveYqlType(column.getType(), column.getValueType(), yqlType, column.getDbTypeQualifier(), column.getCustomValueType()); + } + + @NonNull + private static YqlPrimitiveType resolveYqlType(Type javaType, FieldValueType valueType, + PrimitiveTypeId yqlType, String qualifier, + CustomValueType cvt) { + if (cvt != null && cvt.columnValueType() != valueType) { + throw new IllegalStateException("This should never happen: detected FieldValueType must == @CustomValueType.columnValueType(), but got: " + + valueType + " != " + cvt.columnValueType()); + } + + var underlyingType = resolveYqlType( + cvt != null ? cvt.columnClass() : javaType, + valueType, + yqlType, + qualifier + ); + if (cvt == null) { + return underlyingType; + } + + return new YqlPrimitiveType( + underlyingType.javaType, + underlyingType.yqlType, + (b, o) -> underlyingType.setter.accept(b, preconvert(cvt, o)), + v -> postconvert(cvt, underlyingType.getter.apply(v)) + ); } @NonNull diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java index f72d7ff2..193b0fd4 100644 --- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java +++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/TestYdbRepository.java @@ -19,6 +19,7 @@ import tech.ydb.yoj.repository.test.sample.model.EntityWithValidation; import tech.ydb.yoj.repository.test.sample.model.IndexedEntity; import tech.ydb.yoj.repository.test.sample.model.LogEntry; +import tech.ydb.yoj.repository.test.sample.model.NetworkAppliance; import tech.ydb.yoj.repository.test.sample.model.Primitive; import tech.ydb.yoj.repository.test.sample.model.Project; import tech.ydb.yoj.repository.test.sample.model.Referring; @@ -125,6 +126,11 @@ public Supabubble2Table supabubbles2() { public Table updateFeedEntries() { return table(UpdateFeedEntry.class); } + + @Override + public Table networkAppliances() { + return table(NetworkAppliance.class); + } } private static class YdbSupabubble2Table extends YdbTable implements TestEntityOperations.Supabubble2Table { diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java b/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java index fc711e36..f312f02e 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/EntityIdSchema.java @@ -4,8 +4,8 @@ import com.google.common.reflect.TypeToken; import lombok.NonNull; import tech.ydb.yoj.databind.FieldValueType; +import tech.ydb.yoj.databind.StringValueType; import tech.ydb.yoj.databind.schema.Schema; -import tech.ydb.yoj.databind.schema.StringValueType; import tech.ydb.yoj.databind.schema.configuration.SchemaRegistry; import tech.ydb.yoj.databind.schema.configuration.SchemaRegistry.SchemaKey; import tech.ydb.yoj.databind.schema.naming.NamingStrategy; diff --git a/repository/src/main/java/tech/ydb/yoj/repository/db/common/CommonConverters.java b/repository/src/main/java/tech/ydb/yoj/repository/db/common/CommonConverters.java index 493f0ccb..7a0919cb 100644 --- a/repository/src/main/java/tech/ydb/yoj/repository/db/common/CommonConverters.java +++ b/repository/src/main/java/tech/ydb/yoj/repository/db/common/CommonConverters.java @@ -4,7 +4,12 @@ import lombok.SneakyThrows; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import tech.ydb.yoj.ExperimentalApi; +import tech.ydb.yoj.databind.CustomValueType; +import tech.ydb.yoj.databind.ValueConverter; +import tech.ydb.yoj.databind.schema.CustomConverterException; +import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; import java.lang.reflect.InvocationTargetException; @@ -180,6 +185,37 @@ public static Object fromObject(Type javaType, Object content) { return jsonConverter.fromObject(javaType, content); } + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") + public static Object preconvert(@Nullable CustomValueType cvt, Object value) { + if (cvt != null) { + value = createCustomValueTypeConverter(cvt).toColumn(value); + + Preconditions.checkArgument(cvt.columnClass().isInstance(value), + "Custom value type converter %s must produce a non-null value of type columnClass()=%s but got value of type %s", + cvt.converter().getCanonicalName(), cvt.columnClass().getCanonicalName(), value.getClass().getCanonicalName()); + } + return value; + } + + @ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24") + public static Object postconvert(@Nullable CustomValueType cvt, Object value) { + if (cvt != null) { + value = createCustomValueTypeConverter(cvt).toJava(value); + } + return value; + } + + private static ValueConverter createCustomValueTypeConverter(CustomValueType cvt) { + try { + var ctor = cvt.converter().getConstructor(); + ctor.setAccessible(true); + @SuppressWarnings("unchecked") var converter = (ValueConverter) ctor.newInstance(); + return converter; + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | SecurityException | InvocationTargetException e) { + throw new CustomConverterException(e, "Could not return custom value type converter " + cvt.converter()); + } + } + // TODO: Also standardize Instant and Duration conversion! public interface ThrowingGetter extends Function {