Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#71: Support java.util.UUID natively (as FieldValueType.UUID) #73

Merged
merged 1 commit into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: It's time to migrate this class to interface....

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's a good idea!


@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