Skip to content

Commit

Permalink
#71: Support java.util.UUID natively (as FieldValueType.UUID)
Browse files Browse the repository at this point in the history
Note that this makes backward incompatible method signature changes in `FieldValue.Tuple`
(which should **not** have been used in application code, anyway, but...)
  • Loading branch information
nvamelichev committed Jun 1, 2024
1 parent e8350de commit 443869c
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 84 deletions.
18 changes: 11 additions & 7 deletions databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ public enum FieldValueType {
* Interval. Java-side <strong>must</strong> be an instance of {@link java.time.Duration java.time.Duration}.
*/
INTERVAL,
/**
* Universally Unique Identitifer (UUID). Java-side <strong>must</strong> be an instance of {@link java.util.UUID}.
*/
UUID,
/**
* Binary value: just a stream of uninterpreted bytes.
* Java-side <strong>must</strong> be a {@code byte[]}.
Expand Down Expand Up @@ -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
*/
Expand All @@ -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.
* <p><strong>This method will most likely become package-private in YOJ 3.0.0! Please do not use it outside of YOJ code.</strong>
*
* @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
*/
Expand All @@ -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)) {
Expand Down
145 changes: 109 additions & 36 deletions databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<JavaField> flatFields = schema.flattenFields();
Map<String, Object> flattenedObj = schema.flatten(obj);

List<JavaFieldValue> allFieldValues = flatFields.stream()
.map(jf -> new JavaFieldValue(jf, flattenedObj.get(jf.getName())))
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
@SuppressWarnings({"rawtypes", "unchecked"})
Map<String, Object> flattenedObj = ((ObjectSchema) schema).flatten(obj);

List<FieldAndValue> 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<FieldAndValue> tupleValues(List<JavaField> flatFields, Map<String, Object> 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;
}
Expand Down Expand Up @@ -150,17 +165,18 @@ public boolean isByteArray() {
return byteArray != null;
}

public boolean isUuid() {
return uuid != null;
}

@Nullable
public static Comparable<?> getComparable(@NonNull Map<String, Object> values,
@NonNull JavaField field) {
if (field.isFlat()) {
Object rawValue = values.get(field.getName());
return rawValue == null ? null : ofObj(rawValue, field.toFlatField()).getComparable(field);
} else {
List<JavaFieldValue> 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));
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 "???";
}
}

Expand All @@ -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
Expand All @@ -291,18 +330,52 @@ 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<String, Object> flattenedObj) {
this(jf, getValue(jf, flattenedObj));
}

@Nullable
private static FieldValue getValue(@NonNull JavaField jf, @NonNull Map<String, Object> 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<Tuple> {
@Nullable
@EqualsAndHashCode.Exclude
Object composite;

@NonNull
List<JavaFieldValue> components;
List<FieldAndValue> components;

@NonNull
public Type getType() {
Expand All @@ -317,13 +390,13 @@ public Object asComposite() {
}

@NonNull
public Stream<JavaFieldValue> streamComponents() {
public Stream<FieldAndValue> 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
Expand All @@ -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;
Expand All @@ -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"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
}
Loading

0 comments on commit 443869c

Please sign in to comment.