diff --git a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java
index b06c5b5b..f2ce736b 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueType.java
@@ -2,6 +2,7 @@
import tech.ydb.yoj.ExperimentalApi;
import tech.ydb.yoj.databind.converter.ValueConverter;
+import tech.ydb.yoj.databind.schema.Column;
import tech.ydb.yoj.databind.schema.Schema;
import java.lang.annotation.Inherited;
@@ -16,9 +17,12 @@
/**
* Annotates the class, or entity field/record component as having a custom {@link ValueConverter value converter}.
*
The specified converter will be used by YOJ instead of the default (Database column↔Java field) mapping.
- *
Annotation on entity field/record component has priority over annotation on the field's/record component's class. + *
{@link Column#customValueType() @Column(customValueType=...)} annotation on an entity field/record component + * has priority over annotation on the field's/record component's class. *
This annotation is inherited, so make sure that your {@link #converter() converter} either supports all * possible subclasses of your class, or restrict subclassing by making your class {@code final} or {@code sealed}. + *
Defining recursive custom value types is prohibited: that is, you cannot have a custom value type with + * a converter that returns value of {@link #columnClass() another custom value type}. */ @Inherited @Retention(RUNTIME) @@ -26,14 +30,11 @@ @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(); - - /** - * Exact type of value that {@link #converter() converter's} {@link ValueConverter#toColumn(Schema.JavaField, Object) toColumn()} method returns. - * Must implement {@link Comparable}. + * Class of the values that the {@link ValueConverter#toColumn(Schema.JavaField, Object) toColumn()} method of the {@link #converter() converter} + * returns. + *
Column class itself cannot be a custom value type. It must be one of the {@link FieldValueType database column value types supported by YOJ} + * and it must implement {@link Comparable}. + *
It is allowed to return value of a subclass of {@code columnClass}, e.g. in case of {@code columnClass} being an {@code enum} class.
*/
@SuppressWarnings("rawtypes")
Class extends Comparable> columnClass();
diff --git a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java
index ccad0cfa..15afa734 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/CustomValueTypes.java
@@ -24,8 +24,8 @@ private CustomValueTypes() {
public static Object preconvert(@NonNull JavaField field, @NonNull Object value) {
var cvt = field.getCustomValueType();
if (cvt != null) {
- if (cvt.columnClass().equals(value.getClass())) {
- // Already preconverted
+ if (cvt.columnClass().isInstance(value)) {
+ // Value is already preconverted
return value;
}
@@ -41,11 +41,6 @@ public static Object preconvert(@NonNull JavaField field, @NonNull Object value)
public static Object postconvert(@NonNull JavaField field, @NonNull Object value) {
var cvt = field.getCustomValueType();
if (cvt != null) {
- if (field.getRawType().equals(value.getClass())) {
- // Already postconverted
- return value;
- }
-
value = createCustomValueTypeConverter(cvt).toJava(field, value);
}
return value;
@@ -74,15 +69,31 @@ public static CustomValueType getCustomValueType(@NonNull Type type, @Nullable C
var columnCvt = cvtAnnotation == null || cvtAnnotation.converter().equals(ValueConverter.NoConverter.class) ? null : cvtAnnotation;
var cvt = columnCvt == null ? rawType.getAnnotation(CustomValueType.class) : columnCvt;
if (cvt != null) {
- Preconditions.checkArgument(!cvt.columnValueType().isComposite(), "@CustomValueType.columnValueType must be != COMPOSITE");
- Preconditions.checkArgument(!cvt.columnValueType().isUnknown(), "@CustomValueType.columnValueType must be != UNKNOWN");
+ var columnClass = cvt.columnClass();
+
+ var recursiveCvt = getCustomValueType(columnClass, null);
+ Preconditions.checkArgument(recursiveCvt == null,
+ "Defining recursive custom value types is prohibited, but @CustomValueType.columnClass=%s is annotated with %s",
+ columnClass.getCanonicalName(),
+ recursiveCvt);
+
+ Preconditions.checkArgument(!columnClass.isInterface() && !isAbstract(columnClass.getModifiers()),
+ "@CustomValueType.columnClass=%s must not be an interface or an abstract class", columnClass.getCanonicalName());
+
+ var fvt = FieldValueType.forJavaType(columnClass, null);
+ Preconditions.checkArgument(!fvt.isComposite(),
+ "@CustomValueType.columnClass=%s must not map to FieldValueType.COMPOSITE", columnClass.getCanonicalName());
+ Preconditions.checkArgument(!fvt.isUnknown(),
+ "@CustomValueType.columnClass=%s must not map to FieldValueType.UNKNOWN", columnClass.getCanonicalName());
+
+ var converterClass = cvt.converter();
Preconditions.checkArgument(
- !cvt.converter().equals(ValueConverter.NoConverter.class)
- && !cvt.converter().isInterface()
- && !isAbstract(cvt.converter().getModifiers())
- && (cvt.converter().getDeclaringClass() == null || isStatic(cvt.converter().getModifiers())),
- "@CustomValueType.converter must not be an interface, abstract class, non-static inner class, or NoConverter.class, but got: %s",
- cvt);
+ !converterClass.equals(ValueConverter.NoConverter.class)
+ && !converterClass.isInterface()
+ && !isAbstract(converterClass.getModifiers())
+ && (converterClass.getDeclaringClass() == null || isStatic(converterClass.getModifiers())),
+ "@CustomValueType.converter=%s must not be an interface, abstract class, non-static inner class, or NoConverter.class",
+ converterClass.getCanonicalName());
}
return cvt;
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 af5272d1..325b8a60 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java
@@ -25,30 +25,29 @@
public enum FieldValueType {
/**
* Integer value.
- * Java-side must either be a numeric primitive, or extend {@link Number java.lang.Number} and
- * implement {@link Comparable java.lang.Comparable}.
+ * Java-side must be a {@code long}, {@code int}, {@code short} or {@code byte},
+ * or an instance of their wrapper classes {@code Long}, {@code Integer}, {@code Short} or {@code Byte}.
*/
INTEGER,
/**
* Real (floating-point) number value.
- * Java-side must either be a numeric primitive, or extend {@link Number java.lang.Number} and
- * implement {@link Comparable java.lang.Comparable}.
+ * Java-side must be a {@code double} or a {@code float}, or an instance of their
+ * wrapper classes {@code Double} or {@code Float}.
*/
REAL,
/**
* String value.
- * Java-side must be serialized to simple string value.
+ * Java-side must be a {@code String}.
*/
STRING,
/**
* Boolean value.
- * Java-side must either be an instance of {@link Boolean java.lang.Boolean} or a {@code boolean}
- * primitive.
+ * Java-side must either be a {@code boolean} primitive, or an instance of its
+ * wrapper class {@code Boolean}.
*/
BOOLEAN,
/**
- * Enum value. Java-side must be an instance of {@link Enum java.lang.Enum}.
- * Typically stored as {@link Enum#name() enum constant name} or its {@link Enum#ordinal() ordinal}.
+ * Enum value. Java-side must be a concrete subclass of {@link Enum java.lang.Enum}.
*/
ENUM,
/**
@@ -61,12 +60,14 @@ public enum FieldValueType {
INTERVAL,
/**
* Binary value: just a stream of uninterpreted bytes.
- * Java-side must be a byte array.
+ * Java-side must be a {@code byte[]}.
*
- * @deprecated It is strongly recommended to use a {@link ByteArray} that is properly {@code Comparable}
- * and has a sane {@code equals()}.
+ *
+ * @deprecated Support for mapping raw {@code byte[]} will be removed in YOJ 3.0.0.
+ * Even now, it is strongly recommended to use a {@link ByteArray}: it is properly {@code Comparable}
+ * and has a sane {@code equals()}, which ensures that queries will work the same for in-memory database and YDB.
*/
- @Deprecated
+ @Deprecated(forRemoval = true)
BINARY,
/**
* Binary value: just a stream of uninterpreted bytes.
@@ -75,19 +76,24 @@ public enum FieldValueType {
BYTE_ARRAY,
/**
* Composite value. Can contain any other values, including other composite values.
- * Java-side must be an immutable POJO with all-args constructor, e.g. a Lombok {@code @Value}-annotated
- * class.
+ * Java-side must be an immutable value reflectable by YOJ: a Java {@code Record},
+ * a Kotlin {@code data class}, an immutable POJO with all-args constructor annotated with
+ * {@code @ConstructorProperties} etc.
*/
COMPOSITE,
/**
* Polymorphic object stored in an opaque form (i.e., individual fields cannot be accessed by data binding).
- * Serialized form strongly depends on the the marshalling mechanism (e.g., JSON, YAML, ...).
+ * Serialized form strongly depends on the the marshalling mechanism (e.g., JSON, YAML, ...).
*/
OBJECT,
/**
+ * @deprecated This enum constant will be removed in YOJ 3.0.0; {@link #forJavaType(Type, Column)} will instead
+ * throw an {@code IllegalArgumentException} if an unmappable type is encountered.
+ *
* Value type is unknown.
- * To register a class not in your code (e.g., {@code UUID} from the JDK) as a string-value type, use
- * a {@link Column @Column(customValueType=@CustomValueType(...))} annotation on the specific field.
- *
- * Future versions of YOJ might remove this method entirely.
- *
* @param clazz class to register as string-value. Must either be final or sealed with permissible final-only implementations.
* All permissible implementations of a sealed class will be registered automatically.
+ * @deprecated This method will be removed in YOJ 3.0.0.
+ * Use the {@link CustomValueType} annotation with a {@link StringValueConverter} instead of calling this method.
+ *
+ * To register a class not in your code (e.g., {@code UUID} from the JDK) as a string-value type, use
+ * a {@link Column @Column(customValueType=@CustomValueType(...))} annotation on a specific field.
*/
@Deprecated(forRemoval = true)
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/24")
@@ -145,7 +148,7 @@ public static void registerStringValueType(@NonNull Class> clazz) {
public static FieldValueType forJavaType(Type type, Column columnAnnotation) {
var cvt = CustomValueTypes.getCustomValueType(type, columnAnnotation);
if (cvt != null) {
- return cvt.columnValueType();
+ type = cvt.columnClass();
}
boolean flatten = columnAnnotation == null || columnAnnotation.flatten();
@@ -158,11 +161,6 @@ private static FieldValueType forJavaType(@NonNull Type type) {
if (type instanceof ParameterizedType || type instanceof TypeVariable) {
return OBJECT;
} else if (type instanceof Class> clazz) {
- var cvt = CustomValueTypes.getCustomValueType(clazz, null);
- if (cvt != null) {
- return cvt.columnValueType();
- }
-
if (isStringValueType(clazz)) {
return STRING;
} else if (INTEGER_NUMERIC_TYPES.contains(clazz)) {
@@ -206,10 +204,13 @@ private static boolean isStringValueType(Class> clazz) {
* Checks whether Java object of type {@code type} is mapped to a composite database value
* (i.e. > 1 database field)
*
- * @deprecated This method does not properly take into account the customizations specified in the
- * {@link Column @Column} annotation on the field. Please do not call it directly, instead use
- * {@code FieldValueType.forJavaType(type, column).isComposite()} where {@code column} is the
- * {@link Column @Column} annotation's value.
+ * @deprecated This method will be removed in YOJ 3.0.0.
+ * This method does not properly take into account the customizations specified in the
+ * {@link Column @Column} annotation on the field.
+ * Non-instantiable, every method including the constructor throws {@link UnsupportedOperationException}.
+ */
+ final class NoConverter implements ValueConverter E.g., {@code "XYZ-100500" <=> new Ticket(queue="XYZ", num=100500)}
*/
- @CustomValueType(columnValueType = FieldValueType.STRING, columnClass = String.class, converter = StringValueConverter.class)
+ @CustomValueType(columnClass = String.class, converter = StringValueConverter.class)
public record Ticket(@NonNull String queue, int num) {
public Ticket {
Preconditions.checkArgument(num >= 1, "ticket number must be >= 1");
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 7fe35204..22ca1a89 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
@@ -4,7 +4,7 @@
import lombok.RequiredArgsConstructor;
import lombok.Value;
import tech.ydb.yoj.databind.CustomValueType;
-import tech.ydb.yoj.databind.FieldValueType;
+import tech.ydb.yoj.databind.converter.EnumOrdinalConverter;
import tech.ydb.yoj.databind.converter.StringValueConverter;
import tech.ydb.yoj.databind.schema.Column;
import tech.ydb.yoj.repository.DbTypeQualifier;
@@ -25,15 +25,24 @@ public class UpdateFeedEntry implements Entity
* It might be supported by the data binding implementation, but relying on that fact is not recommended.
*/
+ @Deprecated(forRemoval = true)
UNKNOWN;
private static final Set
Please do not call this method directly, instead use
+ * {@link #forJavaType(Type, Column) FieldValueType.forJavaType(type, column).isComposite()}
+ * where {@code column} is the {@link Column @Column} annotation's value or {@code null} if
+ * there is no annotation/you explicitly don't care.
*
* @param type Java object type
* @return {@code true} if {@code type} maps to a composite database value; {@code false} otherwise
@@ -229,12 +230,28 @@ public boolean isComposite() {
}
/**
+ * @deprecated This method will be removed in YOJ 3.0.0 along with the {@link #UNKNOWN} enum constant.
+ *
* @return {@code true} if there is no fitting database value type for the type provided; {@code false} otherwise
*/
+ @Deprecated(forRemoval = true)
public boolean isUnknown() {
return this == UNKNOWN;
}
+ /**
+ * @deprecated This method will be removed in YOJ 3.0.0. This method is misleadingly named and is not generally useful.
+ *
+ *
+ */
+ @Deprecated(forRemoval = true)
public boolean isSortable() {
return SORTABLE_VALUE_TYPES.contains(this);
}
diff --git a/databind/src/main/java/tech/ydb/yoj/databind/converter/EnumOrdinalConverter.java b/databind/src/main/java/tech/ydb/yoj/databind/converter/EnumOrdinalConverter.java
new file mode 100644
index 00000000..7ef1e2f0
--- /dev/null
+++ b/databind/src/main/java/tech/ydb/yoj/databind/converter/EnumOrdinalConverter.java
@@ -0,0 +1,52 @@
+package tech.ydb.yoj.databind.converter;
+
+import com.google.common.base.Preconditions;
+import lombok.NonNull;
+import tech.ydb.yoj.databind.schema.Schema.JavaField;
+
+/**
+ * A generic converter that can be applied to represent your enum values as their {@link Enum#ordinal() ordinal}s
+ * instead of their {@link Enum#name() constant name}s or {@link Enum#toString() string representation}s.
+ * You can use it in a {@link tech.ydb.yoj.databind.schema.Column @Column} annotation, like this:
+ *
E.g.: Are boolean values sortable or not? They're certainly Comparable.
+ *
E.g.: How do you sort columns with FieldValueType.STRING? Depends on your Locale for in-memory DB and your locale+collation+phase of the moon
+ * for a real database
+ *
etc.
+ * or as a global default for some of your enum type, like this:
+ *
+ * @Column(
+ * customValueType=@CustomValueType(
+ * columnClass=Integer.class,
+ * converter=EnumOrdinalConverter.class
+ * )
+ * )
+ *
+ *
+ * @param
+ * @CustomValueType(
+ * columnClass=Integer.class,
+ * converter=EnumOrdinalConverter.class
+ * )
+ * public enum MyEnum {
+ * FOO,
+ * BAR,
+ * }
+ *
* @Column(
* customValueType=@CustomValueType(
- * columnValueType=STRING,
- * columnClass=<your type>,
+ * columnClass=String.class,
* converter=StringValueConverter.class
* )
* )
diff --git a/databind/src/main/java/tech/ydb/yoj/databind/converter/ValueConverter.java b/databind/src/main/java/tech/ydb/yoj/databind/converter/ValueConverter.java
index bbbf07e9..679a2a8a 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/converter/ValueConverter.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/converter/ValueConverter.java
@@ -8,18 +8,41 @@
* Custom conversion logic between database column values and Java field values.
*
Must have a no-args public constructor.
*
- * @param