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 d115d458..883ec67d 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java
@@ -62,6 +62,10 @@ public enum FieldValueType {
* Interval. Java-side must be an instance of {@link java.time.Duration java.time.Duration}.
*/
INTERVAL,
+ /**
+ * Universally Unique Identitifer (UUID). Java-side must be an instance of {@link java.util.UUID}.
+ */
+ UUID,
/**
* Binary value: just a stream of uninterpreted bytes.
* Java-side must be a {@code byte[]}.
@@ -167,11 +171,10 @@ public static FieldValueType forSchemaField(@NonNull JavaField schemaField) {
* it allows comparing not strictly equal values in filter expressions, e.g., the String value of the ID
* with the (flat) ID itself, which is a wrapper around String.
*
- * @param type Java object type. E.g., {@code String.class} for a String literal from the user
+ * @param type Java object type. E.g., {@code String.class} for a String literal from the user
* @param reflectField reflection information for the Schema field that the object of type {@code type}
- * is supposed to be used with. E.g., reflection information for the (flat) ID field which the String
- * literal is compared with.
- *
+ * is supposed to be used with. E.g., reflection information for the (flat) ID field which the String
+ * literal is compared with.
* @return database value type
* @throws IllegalArgumentException if object of this type cannot be mapped to a database value
*/
@@ -185,10 +188,9 @@ public static FieldValueType forJavaType(@NonNull Type type, @NonNull ReflectFie
* the {@link Column @Column} annotation value as well as custom value type information.
*
This method will most likely become package-private in YOJ 3.0.0! Please do not use it outside of YOJ code.
*
- * @param type Java object type
+ * @param type Java object type
* @param columnAnnotation {@code @Column} annotation for the field; {@code null} if absent
- * @param cvt custom value type information; {@code null} if absent
- *
+ * @param cvt custom value type information; {@code null} if absent
* @return database value type
* @throws IllegalArgumentException if object of this type cannot be mapped to a database value
*/
@@ -210,6 +212,8 @@ public static FieldValueType forJavaType(@NonNull Type type, @Nullable Column co
} else if (type instanceof Class> clazz) {
if (String.class.equals(clazz) || isCustomStringValueType(clazz)) {
return STRING;
+ } else if (java.util.UUID.class.equals(clazz)) {
+ return UUID;
} else if (INTEGER_NUMERIC_TYPES.contains(clazz)) {
return INTEGER;
} else if (REAL_NUMERIC_TYPES.contains(clazz)) {
diff --git a/databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java b/databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java
index 9816a9ec..2b7b817a 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java
@@ -11,20 +11,21 @@
import tech.ydb.yoj.databind.FieldValueType;
import tech.ydb.yoj.databind.schema.ObjectSchema;
import tech.ydb.yoj.databind.schema.Schema.JavaField;
-import tech.ydb.yoj.databind.schema.Schema.JavaFieldValue;
import javax.annotation.Nullable;
import java.lang.reflect.Type;
import java.time.Instant;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.UUID;
import java.util.stream.Stream;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toCollection;
import static lombok.AccessLevel.PRIVATE;
@Value
@@ -37,40 +38,46 @@ public class FieldValue {
Instant timestamp;
Tuple tuple;
ByteArray byteArray;
+ UUID uuid;
@NonNull
public static FieldValue ofStr(@NonNull String str) {
- return new FieldValue(str, null, null, null, null, null, null);
+ return new FieldValue(str, null, null, null, null, null, null, null);
}
@NonNull
public static FieldValue ofNum(long num) {
- return new FieldValue(null, num, null, null, null, null, null);
+ return new FieldValue(null, num, null, null, null, null, null, null);
}
@NonNull
public static FieldValue ofReal(double real) {
- return new FieldValue(null, null, real, null, null, null, null);
+ return new FieldValue(null, null, real, null, null, null, null, null);
}
@NonNull
public static FieldValue ofBool(boolean bool) {
- return new FieldValue(null, null, null, bool, null, null, null);
+ return new FieldValue(null, null, null, bool, null, null, null, null);
}
@NonNull
public static FieldValue ofTimestamp(@NonNull Instant timestamp) {
- return new FieldValue(null, null, null, null, timestamp, null, null);
+ return new FieldValue(null, null, null, null, timestamp, null, null, null);
}
@NonNull
public static FieldValue ofTuple(@NonNull Tuple tuple) {
- return new FieldValue(null, null, null, null, null, tuple, null);
+ return new FieldValue(null, null, null, null, null, tuple, null, null);
}
@NonNull
public static FieldValue ofByteArray(@NonNull ByteArray byteArray) {
- return new FieldValue(null, null, null, null, null, null, byteArray);
+ return new FieldValue(null, null, null, null, null, null, byteArray, null);
+ }
+
+ @NonNull
+ public static FieldValue ofUuid(@NonNull UUID uuid) {
+ return new FieldValue(null, null, null, null, null, null, null, uuid);
}
@NonNull
@@ -100,28 +107,36 @@ public static FieldValue ofObj(@NonNull Object obj, @NonNull JavaField schemaFie
case TIMESTAMP -> {
return ofTimestamp((Instant) obj);
}
+ case UUID -> {
+ return ofUuid((UUID) obj);
+ }
case COMPOSITE -> {
- ObjectSchema schema = ObjectSchema.of(obj.getClass());
+ ObjectSchema> schema = ObjectSchema.of(obj.getClass());
List flatFields = schema.flattenFields();
- Map flattenedObj = schema.flatten(obj);
- List allFieldValues = flatFields.stream()
- .map(jf -> new JavaFieldValue(jf, flattenedObj.get(jf.getName())))
- .collect(collectingAndThen(toList(), Collections::unmodifiableList));
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ Map flattenedObj = ((ObjectSchema) schema).flatten(obj);
+
+ List allFieldValues = tupleValues(flatFields, flattenedObj);
if (allFieldValues.size() == 1) {
- JavaFieldValue singleValue = allFieldValues.iterator().next();
- Preconditions.checkArgument(singleValue.getValue() != null, "Wrappers must have a non-null value inside them");
- return ofObj(singleValue.getValue(), singleValue.getField());
+ FieldValue singleValue = allFieldValues.iterator().next().value();
+ Preconditions.checkArgument(singleValue != null, "Wrappers must have a non-null value inside them");
+ return singleValue;
}
return ofTuple(new Tuple(obj, allFieldValues));
}
- default -> throw new UnsupportedOperationException(
- "Unsupported value type: not a string, integer, timestamp, enum, "
- + "floating-point number, byte array, tuple or wrapper of the above"
- );
+ default -> throw new UnsupportedOperationException("Unsupported value type: not a string, integer, timestamp, UUID, enum, "
+ + "floating-point number, byte array, tuple or wrapper of the above");
}
}
+ private static @NonNull List tupleValues(List flatFields, Map flattenedObj) {
+ return flatFields.stream()
+ .map(jf -> new FieldAndValue(jf, flattenedObj))
+ // Tuple values are allowed to be null, so we explicitly use ArrayList, just make it unmodifiable
+ .collect(collectingAndThen(toCollection(ArrayList::new), Collections::unmodifiableList));
+ }
+
public boolean isNumber() {
return num != null;
}
@@ -150,6 +165,10 @@ public boolean isByteArray() {
return byteArray != null;
}
+ public boolean isUuid() {
+ return uuid != null;
+ }
+
@Nullable
public static Comparable> getComparable(@NonNull Map values,
@NonNull JavaField field) {
@@ -157,10 +176,7 @@ public static Comparable> getComparable(@NonNull Map values,
Object rawValue = values.get(field.getName());
return rawValue == null ? null : ofObj(rawValue, field.toFlatField()).getComparable(field);
} else {
- List components = field.flatten()
- .map(jf -> new JavaFieldValue(jf, values.get(jf.getName())))
- .toList();
- return new Tuple(null, components);
+ return new Tuple(null, tupleValues(field.flatten().toList(), values));
}
}
@@ -221,6 +237,21 @@ public Comparable> getComparable(@NonNull JavaField field) {
}
throw new IllegalStateException("Value cannot be converted to timestamp: " + this);
}
+ case UUID -> {
+ // Compare UUIDs as String representations
+ // Rationale: @see https://devblogs.microsoft.com/oldnewthing/20190913-00/?p=102859
+ if (isUuid()) {
+ return uuid.toString();
+ } else if (isString()) {
+ try {
+ UUID.fromString(str);
+ return str;
+ } catch (IllegalArgumentException ignored) {
+ // ...no-op here because we will throw IllegalStateException right after the try() and if (isString())
+ }
+ }
+ throw new IllegalStateException("Value cannot be converted to UUID: " + this);
+ }
case BOOLEAN -> {
Preconditions.checkState(isBool(), "Value is not a boolean: %s", this);
return bool;
@@ -252,8 +283,14 @@ public String toString() {
return bool.toString();
} else if (isTimestamp()) {
return "#" + timestamp + "#";
- } else {
+ } else if (isByteArray()) {
+ return byteArray.toString();
+ } else if (isTuple()) {
return tuple.toString();
+ } else if (isUuid()) {
+ return "uuid(" + uuid + ")";
+ } else {
+ return "???";
}
}
@@ -272,7 +309,9 @@ public boolean equals(Object o) {
&& Objects.equals(bool, that.bool)
&& Objects.equals(timestamp, that.timestamp)
&& Objects.equals(real, that.real)
- && Objects.equals(tuple, that.tuple);
+ && Objects.equals(tuple, that.tuple)
+ && Objects.equals(byteArray, that.byteArray)
+ && Objects.equals(uuid, that.uuid);
}
@Override
@@ -291,10 +330,44 @@ public int hashCode() {
if (tuple != null) {
result = result * 59 + tuple.hashCode();
}
+ if (byteArray != null) {
+ result = result * 59 + byteArray.hashCode();
+ }
+ if (uuid != null) {
+ result = result * 59 + uuid.hashCode();
+ }
return result;
}
+ public record FieldAndValue(
+ @NonNull JavaField field,
+ @Nullable FieldValue value
+ ) {
+ public FieldAndValue(@NonNull JavaField jf, @NonNull Map flattenedObj) {
+ this(jf, getValue(jf, flattenedObj));
+ }
+
+ @Nullable
+ private static FieldValue getValue(@NonNull JavaField jf, @NonNull Map flattenedObj) {
+ String name = jf.getName();
+ return flattenedObj.containsKey(name) ? FieldValue.ofObj(flattenedObj.get(name), jf) : null;
+ }
+
+ @Nullable
+ public Comparable> toComparable() {
+ return value == null ? null : value.getComparable(field);
+ }
+
+ public Type fieldType() {
+ return field.getType();
+ }
+
+ public String fieldPath() {
+ return field.getPath();
+ }
+ }
+
@Value
public static class Tuple implements Comparable {
@Nullable
@@ -302,7 +375,7 @@ public static class Tuple implements Comparable {
Object composite;
@NonNull
- List components;
+ List components;
@NonNull
public Type getType() {
@@ -317,13 +390,13 @@ public Object asComposite() {
}
@NonNull
- public Stream streamComponents() {
+ public Stream streamComponents() {
return components.stream();
}
@NonNull
public String toString() {
- return components.stream().map(c -> String.valueOf(c.getValue())).collect(joining(", ", "<", ">"));
+ return components.stream().map(fv -> String.valueOf(fv.value())).collect(joining(", ", "<", ">"));
}
@Override
@@ -340,11 +413,11 @@ public int compareTo(@NonNull FieldValue.Tuple other) {
var thisIter = components.iterator();
var otherIter = other.components.iterator();
while (thisIter.hasNext()) {
- JavaFieldValue thisComponent = thisIter.next();
- JavaFieldValue otherComponent = otherIter.next();
+ FieldAndValue thisComponent = thisIter.next();
+ FieldAndValue otherComponent = otherIter.next();
- Object thisValue = thisComponent.getValue();
- Object otherValue = otherComponent.getValue();
+ Comparable> thisValue = thisComponent.toComparable();
+ Comparable> otherValue = otherComponent.toComparable();
// sort null first
if (thisValue == null && otherValue == null) {
continue;
@@ -357,9 +430,9 @@ public int compareTo(@NonNull FieldValue.Tuple other) {
}
Preconditions.checkState(
- thisComponent.getFieldType().equals(otherComponent.getFieldType()),
+ thisComponent.fieldType().equals(otherComponent.fieldType()),
"Different tuple component types at [%s](%s): %s and %s",
- i, thisComponent.getFieldPath(), thisComponent.getFieldType(), otherComponent.getFieldType()
+ i, thisComponent.fieldPath(), thisComponent.fieldType(), otherComponent.fieldType()
);
@SuppressWarnings({"rawtypes", "unchecked"})
diff --git a/databind/src/main/java/tech/ydb/yoj/databind/expression/IllegalExpressionException.java b/databind/src/main/java/tech/ydb/yoj/databind/expression/IllegalExpressionException.java
index a4eeb915..a3d14e34 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/expression/IllegalExpressionException.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/expression/IllegalExpressionException.java
@@ -67,5 +67,11 @@ static final class DateTimeFieldExpected extends FieldTypeError {
super(field, "Type mismatch: cannot compare field \"%s\" with a date-time value"::formatted);
}
}
+
+ static final class UuidFieldExpected extends FieldTypeError {
+ UuidFieldExpected(String field) {
+ super(field, "Type mismatch: cannot compare field \"%s\" with an UUID value"::formatted);
+ }
+ }
}
}
diff --git a/databind/src/main/java/tech/ydb/yoj/databind/expression/ModelField.java b/databind/src/main/java/tech/ydb/yoj/databind/expression/ModelField.java
index 0a3d15d1..ffe04210 100644
--- a/databind/src/main/java/tech/ydb/yoj/databind/expression/ModelField.java
+++ b/databind/src/main/java/tech/ydb/yoj/databind/expression/ModelField.java
@@ -17,6 +17,7 @@
import javax.annotation.Nullable;
import java.lang.reflect.Type;
+import java.util.UUID;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
@@ -68,8 +69,8 @@ public Stream flatten() {
public FieldValue validateValue(@NonNull FieldValue value) {
if (value.isTuple()) {
value.getTuple().streamComponents()
- .filter(jfv -> jfv.getValue() != null)
- .forEach(jfv -> new ModelField(null, jfv.getField()).validateValue(FieldValue.ofObj(jfv.getValue(), jfv.getField())));
+ .filter(jfv -> jfv.value() != null)
+ .forEach(jfv -> new ModelField(null, jfv.field()).validateValue(jfv.value()));
return value;
}
@@ -83,6 +84,17 @@ public FieldValue validateValue(@NonNull FieldValue value) {
checkArgument(enumHasConstant(clazz, enumConstant),
p -> new UnknownEnumConstant(p, enumConstant),
p -> format("Unknown enum constant for field \"%s\": \"%s\"", p, enumConstant));
+ } else if (fieldValueType == FieldValueType.UUID) {
+ String str = value.getStr();
+ try {
+ UUID.fromString(str);
+ } catch (IllegalArgumentException e) {
+ str = null;
+ }
+
+ checkArgument(str != null,
+ IllegalExpressionException.FieldTypeError.UuidFieldExpected::new,
+ p -> format("Not a valid UUID value for field \"%s\"", p));
} else {
checkArgument(fieldValueType == FieldValueType.STRING,
StringFieldExpected::new,
@@ -110,6 +122,10 @@ public FieldValue validateValue(@NonNull FieldValue value) {
checkArgument(fieldValueType == FieldValueType.TIMESTAMP || fieldValueType == FieldValueType.INTEGER,
DateTimeFieldExpected::new,
p -> format("Specified a timestamp value for non-timestamp field \"%s\"", p));
+ } else if (value.isUuid()) {
+ checkArgument(fieldValueType == FieldValueType.UUID || fieldValueType == FieldValueType.STRING,
+ IllegalExpressionException.FieldTypeError.UuidFieldExpected::new,
+ p -> format("Specified an UUID value for non-UUID/non-String field \"%s\"", p));
} else {
throw new UnsupportedOperationException("Unsupported field value type. This should never happen!");
}
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 bcc55b74..99f594c2 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
@@ -77,14 +77,14 @@ private static Object serialize(Schema.JavaField field, Object value) {
value = CustomValueTypes.preconvert(field, value);
return switch (field.getValueType()) {
- case STRING -> value;
+ case STRING, BOOLEAN, INTEGER, REAL -> value;
+ case UUID -> CommonConverters.serializeUuidValue(value);
case ENUM -> DbTypeQualifier.ENUM_TO_STRING.equals(qualifier)
? CommonConverters.serializeEnumToStringValue(serializedType, value)
: CommonConverters.serializeEnumValue(serializedType, value);
case OBJECT -> CommonConverters.serializeOpaqueObjectValue(serializedType, value);
case BINARY -> ((byte[]) value).clone();
case BYTE_ARRAY -> ((ByteArray) value).copy().getArray();
- case BOOLEAN, INTEGER, REAL -> value;
// TODO: Unify Instant and Duration handling in InMemory and YDB Repository
case INTERVAL, TIMESTAMP -> value;
default -> throw new IllegalStateException("Don't know how to serialize field: " + field);
@@ -105,14 +105,14 @@ private static Object deserialize(Schema.JavaField field, Object value) {
Preconditions.checkState(field.isSimple(), "Trying to deserialize a non-simple field: %s", field);
var deserialized = switch (field.getValueType()) {
- case STRING -> value;
+ case STRING, BOOLEAN, INTEGER, REAL -> value;
+ case UUID -> CommonConverters.deserializeUuidValue(value);
case ENUM -> DbTypeQualifier.ENUM_TO_STRING.equals(qualifier)
? CommonConverters.deserializeEnumToStringValue(serializedType, value)
: CommonConverters.deserializeEnumValue(serializedType, value);
case OBJECT -> CommonConverters.deserializeOpaqueObjectValue(serializedType, value);
case BINARY -> ((byte[]) value).clone();
case BYTE_ARRAY -> ByteArray.copy((byte[]) value);
- case BOOLEAN, INTEGER, REAL -> value;
// TODO: Unify Instant and Duration handling in InMemory and YDB Repository
case INTERVAL, TIMESTAMP -> value;
default -> throw new IllegalStateException("Don't know how to deserialize field: " + field);
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 29398fac..5152e1c3 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
@@ -1,7 +1,6 @@
package tech.ydb.yoj.repository.test.entity;
import lombok.NonNull;
-import tech.ydb.yoj.databind.FieldValueType;
import tech.ydb.yoj.repository.db.Entity;
import tech.ydb.yoj.repository.db.Repository;
import tech.ydb.yoj.repository.test.sample.model.Book;
@@ -28,7 +27,6 @@
import tech.ydb.yoj.repository.test.sample.model.WithUnflattenableField;
import java.util.List;
-import java.util.UUID;
public final class TestEntities {
private TestEntities() {
@@ -57,9 +55,6 @@ private TestEntities() {
@SuppressWarnings("unchecked")
public static Repository init(@NonNull Repository repository) {
- // Intentional Legacy registration. Used in e.g. UniqueEntity
- FieldValueType.registerStringValueType(UUID.class);
-
repository.createTablespace();
ALL.forEach(entityClass -> repository.schema(entityClass).create());
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 9c722233..b9403b18 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
@@ -35,6 +35,7 @@
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
@@ -48,6 +49,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.uuidValueGetter;
+import static tech.ydb.yoj.repository.db.common.CommonConverters.uuidValueSetter;
@Value
@AllArgsConstructor(access = PRIVATE)
@@ -106,6 +109,8 @@ public class YqlPrimitiveType implements YqlType {
private static final Setter DURATION_SECOND_SETTER = (b, v) -> b.setInt32Value(Math.toIntExact(((Duration) v).toSeconds()));
private static final Setter DURATION_SECOND_UINT_SETTER = (b, v) -> b.setUint32Value(Math.toIntExact(((Duration) v).toSeconds()));
private static final Setter DURATION_UTF8_SETTER = (b, v) -> b.setTextValue(((Duration) v).truncatedTo(ChronoUnit.MICROS).toString());
+ private static final Setter UUID_STRING_SETTER = uuidValueSetter(STRING_SETTER)::accept;
+ private static final Setter UUID_UTF8_SETTER = uuidValueSetter(TEXT_SETTER)::accept;
private static final Function ENUM_NAME_STRING_SETTERS = type -> enumValueSetter(type, STRING_SETTER)::accept;
private static final Function ENUM_NAME_UTF8_SETTERS = type -> enumValueSetter(type, TEXT_SETTER)::accept;
@@ -146,6 +151,8 @@ public class YqlPrimitiveType implements YqlType {
private static final Getter DURATION_SECOND_GETTER = v -> Duration.ofSeconds(v.getInt32Value());
private static final Getter DURATION_SECOND_UINT_GETTER = v -> Duration.ofSeconds(v.getUint32Value());
private static final Getter DURATION_UTF8_GETTER = v -> Duration.parse(v.getTextValue());
+ private static final Getter UUID_STRING_GETTER = uuidValueGetter(STRING_GETTER)::apply;
+ private static final Getter UUID_UTF8_GETTER = uuidValueGetter(TEXT_GETTER)::apply;
private static final Getter CONTAINER_VALUE_GETTER = new YqlPrimitiveType.YdbContainerValueGetter();
@@ -200,6 +207,9 @@ public class YqlPrimitiveType implements YqlType {
registerYqlType(Duration.class, PrimitiveTypeId.UINT32, null, false, DURATION_SECOND_UINT_SETTER, DURATION_SECOND_UINT_GETTER);
registerYqlType(Duration.class, PrimitiveTypeId.UTF8, null, false, DURATION_UTF8_SETTER, DURATION_UTF8_GETTER);
+ registerYqlType(UUID.class, PrimitiveTypeId.UTF8, null, true, UUID_UTF8_SETTER, UUID_UTF8_GETTER);
+ registerYqlType(UUID.class, PrimitiveTypeId.STRING, null, false, UUID_STRING_SETTER, UUID_STRING_GETTER);
+
registerPrimitiveTypes();
registerYqlType(FieldValueType.STRING, PrimitiveTypeId.STRING, null, true, STRING_VALUE_STRING_SETTERS, STRING_VALUE_STRING_GETTERS);
@@ -341,36 +351,42 @@ private static void registerYqlType(
/**
* @deprecated This method will be removed in YOJ 3.0.0.
- * Call {@link #useRecommendedMappingFor(FieldValueType[]) useNewMappingFor(STRING, ENUM)} instead.
+ * Call {@link #useRecommendedMappingFor(FieldValueType[]) useNewMappingFor(STRING, ENUM, UUID)} instead, if you wish to map Strings, Enums and
+ * UUIDs to {@code UTF8} ({@code TEXT}) YDB column type (i.e., UTF-8 encoded text).
*/
@Deprecated(forRemoval = true)
public static void changeStringDefaultTypeToUtf8() {
DeprecationWarnings.warnOnce("YqlPrimitiveType.changeStringDefaultTypeToUtf8()",
- "You are using YqlPrimitiveType.changeStringDefaultTypeToUtf8() which will be removed in YOJ 3.0.0. "
- + "Please use YqlPrimitiveType.useNewMappingFor(STRING, ENUM)");
- useRecommendedMappingFor(FieldValueType.STRING, FieldValueType.ENUM);
+ "You are using YqlPrimitiveType.changeStringDefaultTypeToUtf8() which will be removed in YOJ 3.0.0."
+ + "Please use YqlPrimitiveType.useNewMappingFor(STRING, ENUM, UUID) if you wish to use to map Strings, Enums and UUIDs "
+ + "to `UTF8` (`TEXT`) YDB column type (i.e., UTF-8 encoded text).");
+ useRecommendedMappingFor(FieldValueType.STRING, FieldValueType.ENUM, FieldValueType.UUID);
}
/**
* @deprecated This method has a misleading name and will be removed in YOJ 3.0.0.
- * Call {@link #useLegacyMappingFor(FieldValueType[]) useLegacyMappingFor(STRING, ENUM)} instead.
+ * Call {@link #useLegacyMappingFor(FieldValueType[]) useLegacyMappingFor(STRING, ENUM, UUID)} instead, if you wish to map Strings, Enums and
+ * UUIDs to {@code STRING} ({@code BYTES}) YDB column type (i.e., a byte array).
*/
@Deprecated(forRemoval = true)
public static void resetStringDefaultTypeToDefaults() {
DeprecationWarnings.warnOnce("YqlPrimitiveType.resetStringDefaultTypeToDefaults()",
"You are using YqlPrimitiveType.resetStringDefaultTypeToDefaults() which will be removed in YOJ 3.0.0. "
- + "Please use YqlPrimitiveType.useLegacyMappingFor(STRING, ENUM)");
- useLegacyMappingFor(FieldValueType.STRING, FieldValueType.ENUM);
+ + "Please use YqlPrimitiveType.useLegacyMappingFor(STRING, ENUM, UUID) if you wish to use to map Strings, Enums and UUIDs "
+ + "to `STRING` (`BYTES`) YDB column type (i.e., byte array).");
+ useLegacyMappingFor(FieldValueType.STRING, FieldValueType.ENUM, FieldValueType.UUID);
}
/**
* Uses the legacy (YOJ 1.0.x) field value type ↔ YDB column type mapping for the specified field value type(s).
- * If you need to support legacy applications, call {@code useLegacyMappingFor(STRING, ENUM, TIMESTAMP)} before using
+ * If you need to support a wide range of legacy applications, call {@code useLegacyMappingFor(FieldValueType.values())} before using
* any YOJ features.
+ *
You can apply the legacy mapping partially, e.g. {@code useLegacyMappingFor(STRING, ENUM, UUID)} to map String, Enums and UUIDs
+ * to {@code STRING} ({@code BYTES}) YDB column type (i.e., a byte array).
*
* @param fieldValueTypes field value types to use legacy mapping for
* @deprecated We STRONGLY advise against using the legacy mapping in new projects.
- * Please call {@link #useRecommendedMappingFor(FieldValueType...) useNewMappingFor(STRING, ENUM, TIMESTAMP)} instead,
+ * Please call {@link #useRecommendedMappingFor(FieldValueType...) useNewMappingFor(FieldValueType.values())} instead,
* and annotate custom-mapped columns with {@link Column @Column} where a different mapping is desired.
*/
@Deprecated
@@ -379,12 +395,16 @@ public static void useLegacyMappingFor(FieldValueType... fieldValueTypes) {
for (var fvt : fieldValueTypes) {
switch (fvt) {
case STRING, ENUM -> VALUE_DEFAULT_YQL_TYPES.put(fvt, new ValueYqlTypeSelector(fvt, PrimitiveTypeId.STRING, null));
+ case UUID -> {
+ var selector = new YqlTypeSelector(Instant.class, PrimitiveTypeId.STRING, null);
+ JAVA_DEFAULT_YQL_TYPES.put(UUID.class, selector);
+ YQL_TYPES.put(selector, new YqlPrimitiveType(UUID.class, PrimitiveTypeId.STRING, UUID_STRING_SETTER, UUID_STRING_GETTER));
+ }
case TIMESTAMP -> {
var selector = new YqlTypeSelector(Instant.class, PrimitiveTypeId.INT64, null);
JAVA_DEFAULT_YQL_TYPES.put(Instant.class, selector);
YQL_TYPES.put(selector, new YqlPrimitiveType(Instant.class, PrimitiveTypeId.INT64, INSTANT_SETTER, INSTANT_GETTER));
}
- default -> throw new IllegalArgumentException("There is no legacy mapping for field value type: " + fvt);
}
}
}
@@ -392,9 +412,11 @@ public static void useLegacyMappingFor(FieldValueType... fieldValueTypes) {
/**
* Uses the recommended field value type ↔ YDB column type mapping for the specified field value type(s).
*
- * In new projects, we STRONGLY advise that you call {@code useNewMappingFor(STRING, ENUM, TIMESTAMP)}
- * before using any YOJ features. This will eventually become the default mapping, and the call will become a no-op and
- * mighe even be removed.
+ * In new projects, we STRONGLY advise you to call {@code useNewMappingFor(FieldValueType.values())} before using
+ * any YOJ features. This will eventually become the default mapping, and this call will become a no-op and might even be removed
+ * in a future version of YOJ.
+ *
You can apply the new mapping partially, e.g. {@code useNewMappingFor(STRING, ENUM, UUID)} to map String, Enums and UUIDs
+ * to {@code UTF8} ({@code TEXT}) YDB column type (i.e., UTF-8 encoded text).
*
* @param fieldValueTypes field value types to use the new mapping for
*/
@@ -404,12 +426,16 @@ public static void useRecommendedMappingFor(FieldValueType... fieldValueTypes) {
for (var fvt : fieldValueTypes) {
switch (fvt) {
case STRING, ENUM -> VALUE_DEFAULT_YQL_TYPES.put(fvt, new ValueYqlTypeSelector(fvt, PrimitiveTypeId.UTF8, null));
+ case UUID -> {
+ var selector = new YqlTypeSelector(Instant.class, PrimitiveTypeId.UTF8, null);
+ JAVA_DEFAULT_YQL_TYPES.put(UUID.class, selector);
+ YQL_TYPES.put(selector, new YqlPrimitiveType(UUID.class, PrimitiveTypeId.UTF8, UUID_UTF8_SETTER, UUID_UTF8_GETTER));
+ }
case TIMESTAMP -> {
var selector = new YqlTypeSelector(Instant.class, PrimitiveTypeId.TIMESTAMP, null);
JAVA_DEFAULT_YQL_TYPES.put(Instant.class, selector);
YQL_TYPES.put(selector, new YqlPrimitiveType(Instant.class, PrimitiveTypeId.TIMESTAMP, TIMESTAMP_SETTER, TIMESTAMP_GETTER));
}
- default -> throw new IllegalArgumentException("There is no new mapping for field value type: " + fvt);
}
}
}
diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java
index c560f516..88ef496d 100644
--- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java
+++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesLegacyMappingTest.java
@@ -26,9 +26,6 @@
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static tech.ydb.yoj.databind.FieldValueType.ENUM;
-import static tech.ydb.yoj.databind.FieldValueType.STRING;
-import static tech.ydb.yoj.databind.FieldValueType.TIMESTAMP;
@RunWith(Parameterized.class)
public class YqlTypeAllTypesLegacyMappingTest {
@@ -42,7 +39,7 @@ public class YqlTypeAllTypesLegacyMappingTest {
@BeforeClass
public static void setUp() {
- YqlPrimitiveType.useLegacyMappingFor(STRING, ENUM, TIMESTAMP);
+ YqlPrimitiveType.useLegacyMappingFor(FieldValueType.values());
}
@Parameters
diff --git a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java
index 3d0821d3..8b9bb115 100644
--- a/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java
+++ b/repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/yql/YqlTypeAllTypesRecommendedMappingTest.java
@@ -27,9 +27,6 @@
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static tech.ydb.yoj.databind.FieldValueType.ENUM;
-import static tech.ydb.yoj.databind.FieldValueType.STRING;
-import static tech.ydb.yoj.databind.FieldValueType.TIMESTAMP;
@RunWith(Parameterized.class)
public class YqlTypeAllTypesRecommendedMappingTest {
@@ -43,12 +40,12 @@ public class YqlTypeAllTypesRecommendedMappingTest {
@BeforeClass
public static void setUp() {
- YqlPrimitiveType.useRecommendedMappingFor(STRING, ENUM, TIMESTAMP);
+ YqlPrimitiveType.useRecommendedMappingFor(FieldValueType.values());
}
@AfterClass
public static void tearDown() {
- YqlPrimitiveType.useLegacyMappingFor(STRING, ENUM, TIMESTAMP);
+ YqlPrimitiveType.useLegacyMappingFor(FieldValueType.values());
}
@Parameters
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 1ca0ff12..161e0437 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
@@ -24,6 +24,7 @@
import static tech.ydb.yoj.databind.FieldValueType.INTEGER;
import static tech.ydb.yoj.databind.FieldValueType.STRING;
import static tech.ydb.yoj.databind.FieldValueType.TIMESTAMP;
+import static tech.ydb.yoj.databind.FieldValueType.UUID;
import static tech.ydb.yoj.databind.schema.naming.NamingStrategy.NAME_DELIMITER;
public final class EntityIdSchema> extends Schema implements Comparator {
@@ -54,7 +55,7 @@ public static > Comparator> getIdComparator(Cla
private static final Type ENTITY_TYPE_PARAMETER = Entity.Id.class.getTypeParameters()[0];
private static final Set ALLOWED_ID_FIELD_TYPES = Set.of(
- STRING, INTEGER, ENUM, BOOLEAN, TIMESTAMP, BYTE_ARRAY
+ STRING, INTEGER, ENUM, BOOLEAN, TIMESTAMP, UUID, BYTE_ARRAY
);
private > EntityIdSchema(EntitySchema entitySchema) {
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 7dbd2c4f..c41c99ad 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
@@ -9,6 +9,7 @@
import java.lang.reflect.Type;
import java.util.Locale;
import java.util.Map;
+import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Stream;
@@ -74,12 +75,10 @@ public static ThrowingSetter enumToStringValueSetter(Type type, BiConsume
return (d, v) -> rawValueSetter.accept(d, serializeEnumToStringValue(type, v));
}
- public static String serializeEnumToStringValue(Type type, Object v) {
- if (v instanceof Enum || v instanceof String) {
- return v.toString();
- } else {
- throw new IllegalArgumentException("Enum value should be Enum or String but is " + type.getTypeName());
- }
+ public static String serializeEnumToStringValue(Type ignored, Object v) {
+ Preconditions.checkArgument(v instanceof Enum || v instanceof String,
+ "Enum value must be a subclass of java.lang.Enum or a java.lang.String but is %s", v.getClass().getName());
+ return v.toString();
}
public static Object deserializeEnumToStringValue(Type type, Object src) {
@@ -97,6 +96,31 @@ public static ThrowingGetter enumToStringValueGetter(Type type, Function<
return v -> enumValues.get(((String) rawValueGetter.apply(v)));
}
+ public static ThrowingSetter uuidValueSetter(BiConsumer rawValueSetter) {
+ return (d, v) -> rawValueSetter.accept(d, serializeUuidValue(v));
+ }
+
+ // Intentional: Java UUID's compareTo() has a very unique (and very unexpected) ordering, treating two longs comprising the UUID as *signed*!
+ // So we always represent UUIDs in the database as text values, which has fairly consistent ordering in both Java and YDB.
+ // @see https://devblogs.microsoft.com/oldnewthing/20190913-00/?p=102859
+ public static String serializeUuidValue(Object v) {
+ Preconditions.checkArgument(v instanceof UUID || v instanceof String,
+ "Value must be an instance of java.util.UUID or a java.lang.String but is %s", v.getClass().getName());
+ return v.toString();
+ }
+
+ public static ThrowingGetter uuidValueGetter(Function rawValueGetter) {
+ return v -> deserializeUuidValue(rawValueGetter.apply(v));
+ }
+
+ public static Object deserializeUuidValue(Object v) {
+ if (v instanceof String str) {
+ return UUID.fromString(str);
+ } else {
+ throw new IllegalArgumentException("Value must be an instance of java.lang.String, got value of type " + v.getClass().getName());
+ }
+ }
+
public static ThrowingSetter opaqueObjectValueSetter(Type type, BiConsumer rawValueSetter) {
return (d, v) -> rawValueSetter.accept(d, serializeOpaqueObjectValue(type, v));
}