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)); }