Skip to content

Commit

Permalink
#24: EXPERIMENTAL: Allow to have converters between YOJ-supported fie…
Browse files Browse the repository at this point in the history
…ld value types and custom user types

See the `@CustomValueType` annotation and the `ValueConverter` interface for more information.
  • Loading branch information
nvamelichev committed Feb 12, 2024
1 parent 3d97e1d commit 6b0a89c
Show file tree
Hide file tree
Showing 19 changed files with 274 additions and 10 deletions.
30 changes: 30 additions & 0 deletions databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java
Original file line number Diff line number Diff line change
@@ -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<? extends ValueConverter<?, ?>> converter();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -156,6 +155,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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package tech.ydb.yoj.databind.schema;
package tech.ydb.yoj.databind;

import tech.ydb.yoj.ExperimentalApi;

Expand Down
19 changes: 19 additions & 0 deletions databind/src/main/java/tech/ydb/yoj/databind/ValueConverter.java
Original file line number Diff line number Diff line change
@@ -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 <V> Java value type
* @param <C> Database column value type
*/
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
public interface ValueConverter<V, C> {
@NonNull
C toColumn(@NonNull V v);

@NonNull
V toJava(@NonNull C c);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -695,6 +698,12 @@ private JavaField findField(List<String> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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)
Expand All @@ -95,7 +98,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)
Expand All @@ -107,6 +111,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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -106,6 +107,11 @@ public Supabubble2Table supabubbles2() {
public Table<UpdateFeedEntry> updateFeedEntries() {
return table(UpdateFeedEntry.class);
}

@Override
public Table<NetworkAppliance> networkAppliances() {
return table(NetworkAppliance.class);
}
}

private static class Supabubble2InMemoryTable extends InMemoryTable<Supabubble2> implements TestEntityOperations.Supabubble2Table {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,6 +40,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;
Expand Down Expand Up @@ -2589,6 +2591,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<RepositoryTransaction> action) {
// We do not retry transactions, because we do not expect conflicts in our test scenarios.
RepositoryTransaction transaction = startTransaction();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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;
Expand Down Expand Up @@ -39,7 +40,8 @@ private TestEntities() {
Supabubble2.class,
NonDeserializableEntity.class,
WithUnflattenableField.class,
UpdateFeedEntry.class
UpdateFeedEntry.class,
NetworkAppliance.class
);

@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.Primitive;
import tech.ydb.yoj.repository.test.sample.model.Project;
import tech.ydb.yoj.repository.test.sample.model.Referring;
Expand Down Expand Up @@ -55,6 +56,8 @@ public interface TestEntityOperations extends BaseDb {

Table<UpdateFeedEntry> updateFeedEntries();

Table<NetworkAppliance> networkAppliances();

class ProjectTable extends AbstractDelegatingTable<Project> {
public ProjectTable(Table<Project> target) {
super(target);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<NetworkAppliance> {
public record Id(@NonNull String value) implements RecordEntity.Id<NetworkAppliance> {
}

@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<Ipv6Address, byte[]> {
@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);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,6 +13,7 @@
import lombok.NonNull;
import lombok.Value;
import lombok.With;
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;
Expand Down Expand Up @@ -39,6 +41,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;

Expand Down Expand Up @@ -328,7 +332,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);
}

/**
Expand All @@ -344,7 +350,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,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;
Expand Down Expand Up @@ -125,6 +126,11 @@ public Supabubble2Table supabubbles2() {
public Table<UpdateFeedEntry> updateFeedEntries() {
return table(UpdateFeedEntry.class);
}

@Override
public Table<NetworkAppliance> networkAppliances() {
return table(NetworkAppliance.class);
}
}

private static class YdbSupabubble2Table extends YdbTable<Supabubble2> implements Supabubble2Table {
Expand Down
Loading

0 comments on commit 6b0a89c

Please sign in to comment.