Skip to content

Commit 443869c

Browse files
committed
#71: Support java.util.UUID natively (as FieldValueType.UUID)
Note that this makes backward incompatible method signature changes in `FieldValue.Tuple` (which should **not** have been used in application code, anyway, but...)
1 parent e8350de commit 443869c

File tree

11 files changed

+223
-84
lines changed

11 files changed

+223
-84
lines changed

databind/src/main/java/tech/ydb/yoj/databind/FieldValueType.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public enum FieldValueType {
6262
* Interval. Java-side <strong>must</strong> be an instance of {@link java.time.Duration java.time.Duration}.
6363
*/
6464
INTERVAL,
65+
/**
66+
* Universally Unique Identitifer (UUID). Java-side <strong>must</strong> be an instance of {@link java.util.UUID}.
67+
*/
68+
UUID,
6569
/**
6670
* Binary value: just a stream of uninterpreted bytes.
6771
* Java-side <strong>must</strong> be a {@code byte[]}.
@@ -167,11 +171,10 @@ public static FieldValueType forSchemaField(@NonNull JavaField schemaField) {
167171
* it allows comparing not strictly equal values in filter expressions, e.g., the String value of the ID
168172
* with the (flat) ID itself, which is a wrapper around String.
169173
*
170-
* @param type Java object type. E.g., {@code String.class} for a String literal from the user
174+
* @param type Java object type. E.g., {@code String.class} for a String literal from the user
171175
* @param reflectField reflection information for the Schema field that the object of type {@code type}
172-
* is supposed to be used with. E.g., reflection information for the (flat) ID field which the String
173-
* literal is compared with.
174-
*
176+
* is supposed to be used with. E.g., reflection information for the (flat) ID field which the String
177+
* literal is compared with.
175178
* @return database value type
176179
* @throws IllegalArgumentException if object of this type cannot be mapped to a database value
177180
*/
@@ -185,10 +188,9 @@ public static FieldValueType forJavaType(@NonNull Type type, @NonNull ReflectFie
185188
* the {@link Column @Column} annotation value as well as custom value type information.
186189
* <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>
187190
*
188-
* @param type Java object type
191+
* @param type Java object type
189192
* @param columnAnnotation {@code @Column} annotation for the field; {@code null} if absent
190-
* @param cvt custom value type information; {@code null} if absent
191-
*
193+
* @param cvt custom value type information; {@code null} if absent
192194
* @return database value type
193195
* @throws IllegalArgumentException if object of this type cannot be mapped to a database value
194196
*/
@@ -210,6 +212,8 @@ public static FieldValueType forJavaType(@NonNull Type type, @Nullable Column co
210212
} else if (type instanceof Class<?> clazz) {
211213
if (String.class.equals(clazz) || isCustomStringValueType(clazz)) {
212214
return STRING;
215+
} else if (java.util.UUID.class.equals(clazz)) {
216+
return UUID;
213217
} else if (INTEGER_NUMERIC_TYPES.contains(clazz)) {
214218
return INTEGER;
215219
} else if (REAL_NUMERIC_TYPES.contains(clazz)) {

databind/src/main/java/tech/ydb/yoj/databind/expression/FieldValue.java

Lines changed: 109 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@
1111
import tech.ydb.yoj.databind.FieldValueType;
1212
import tech.ydb.yoj.databind.schema.ObjectSchema;
1313
import tech.ydb.yoj.databind.schema.Schema.JavaField;
14-
import tech.ydb.yoj.databind.schema.Schema.JavaFieldValue;
1514

1615
import javax.annotation.Nullable;
1716
import java.lang.reflect.Type;
1817
import java.time.Instant;
18+
import java.util.ArrayList;
1919
import java.util.Collections;
2020
import java.util.List;
2121
import java.util.Map;
2222
import java.util.Objects;
23+
import java.util.UUID;
2324
import java.util.stream.Stream;
2425

2526
import static java.util.stream.Collectors.collectingAndThen;
2627
import static java.util.stream.Collectors.joining;
27-
import static java.util.stream.Collectors.toList;
28+
import static java.util.stream.Collectors.toCollection;
2829
import static lombok.AccessLevel.PRIVATE;
2930

3031
@Value
@@ -37,40 +38,46 @@ public class FieldValue {
3738
Instant timestamp;
3839
Tuple tuple;
3940
ByteArray byteArray;
41+
UUID uuid;
4042

4143
@NonNull
4244
public static FieldValue ofStr(@NonNull String str) {
43-
return new FieldValue(str, null, null, null, null, null, null);
45+
return new FieldValue(str, null, null, null, null, null, null, null);
4446
}
4547

4648
@NonNull
4749
public static FieldValue ofNum(long num) {
48-
return new FieldValue(null, num, null, null, null, null, null);
50+
return new FieldValue(null, num, null, null, null, null, null, null);
4951
}
5052

5153
@NonNull
5254
public static FieldValue ofReal(double real) {
53-
return new FieldValue(null, null, real, null, null, null, null);
55+
return new FieldValue(null, null, real, null, null, null, null, null);
5456
}
5557

5658
@NonNull
5759
public static FieldValue ofBool(boolean bool) {
58-
return new FieldValue(null, null, null, bool, null, null, null);
60+
return new FieldValue(null, null, null, bool, null, null, null, null);
5961
}
6062

6163
@NonNull
6264
public static FieldValue ofTimestamp(@NonNull Instant timestamp) {
63-
return new FieldValue(null, null, null, null, timestamp, null, null);
65+
return new FieldValue(null, null, null, null, timestamp, null, null, null);
6466
}
6567

6668
@NonNull
6769
public static FieldValue ofTuple(@NonNull Tuple tuple) {
68-
return new FieldValue(null, null, null, null, null, tuple, null);
70+
return new FieldValue(null, null, null, null, null, tuple, null, null);
6971
}
7072

7173
@NonNull
7274
public static FieldValue ofByteArray(@NonNull ByteArray byteArray) {
73-
return new FieldValue(null, null, null, null, null, null, byteArray);
75+
return new FieldValue(null, null, null, null, null, null, byteArray, null);
76+
}
77+
78+
@NonNull
79+
public static FieldValue ofUuid(@NonNull UUID uuid) {
80+
return new FieldValue(null, null, null, null, null, null, null, uuid);
7481
}
7582

7683
@NonNull
@@ -100,28 +107,36 @@ public static FieldValue ofObj(@NonNull Object obj, @NonNull JavaField schemaFie
100107
case TIMESTAMP -> {
101108
return ofTimestamp((Instant) obj);
102109
}
110+
case UUID -> {
111+
return ofUuid((UUID) obj);
112+
}
103113
case COMPOSITE -> {
104-
ObjectSchema schema = ObjectSchema.of(obj.getClass());
114+
ObjectSchema<?> schema = ObjectSchema.of(obj.getClass());
105115
List<JavaField> flatFields = schema.flattenFields();
106-
Map<String, Object> flattenedObj = schema.flatten(obj);
107116

108-
List<JavaFieldValue> allFieldValues = flatFields.stream()
109-
.map(jf -> new JavaFieldValue(jf, flattenedObj.get(jf.getName())))
110-
.collect(collectingAndThen(toList(), Collections::unmodifiableList));
117+
@SuppressWarnings({"rawtypes", "unchecked"})
118+
Map<String, Object> flattenedObj = ((ObjectSchema) schema).flatten(obj);
119+
120+
List<FieldAndValue> allFieldValues = tupleValues(flatFields, flattenedObj);
111121
if (allFieldValues.size() == 1) {
112-
JavaFieldValue singleValue = allFieldValues.iterator().next();
113-
Preconditions.checkArgument(singleValue.getValue() != null, "Wrappers must have a non-null value inside them");
114-
return ofObj(singleValue.getValue(), singleValue.getField());
122+
FieldValue singleValue = allFieldValues.iterator().next().value();
123+
Preconditions.checkArgument(singleValue != null, "Wrappers must have a non-null value inside them");
124+
return singleValue;
115125
}
116126
return ofTuple(new Tuple(obj, allFieldValues));
117127
}
118-
default -> throw new UnsupportedOperationException(
119-
"Unsupported value type: not a string, integer, timestamp, enum, "
120-
+ "floating-point number, byte array, tuple or wrapper of the above"
121-
);
128+
default -> throw new UnsupportedOperationException("Unsupported value type: not a string, integer, timestamp, UUID, enum, "
129+
+ "floating-point number, byte array, tuple or wrapper of the above");
122130
}
123131
}
124132

133+
private static @NonNull List<FieldAndValue> tupleValues(List<JavaField> flatFields, Map<String, Object> flattenedObj) {
134+
return flatFields.stream()
135+
.map(jf -> new FieldAndValue(jf, flattenedObj))
136+
// Tuple values are allowed to be null, so we explicitly use ArrayList, just make it unmodifiable
137+
.collect(collectingAndThen(toCollection(ArrayList::new), Collections::unmodifiableList));
138+
}
139+
125140
public boolean isNumber() {
126141
return num != null;
127142
}
@@ -150,17 +165,18 @@ public boolean isByteArray() {
150165
return byteArray != null;
151166
}
152167

168+
public boolean isUuid() {
169+
return uuid != null;
170+
}
171+
153172
@Nullable
154173
public static Comparable<?> getComparable(@NonNull Map<String, Object> values,
155174
@NonNull JavaField field) {
156175
if (field.isFlat()) {
157176
Object rawValue = values.get(field.getName());
158177
return rawValue == null ? null : ofObj(rawValue, field.toFlatField()).getComparable(field);
159178
} else {
160-
List<JavaFieldValue> components = field.flatten()
161-
.map(jf -> new JavaFieldValue(jf, values.get(jf.getName())))
162-
.toList();
163-
return new Tuple(null, components);
179+
return new Tuple(null, tupleValues(field.flatten().toList(), values));
164180
}
165181
}
166182

@@ -221,6 +237,21 @@ public Comparable<?> getComparable(@NonNull JavaField field) {
221237
}
222238
throw new IllegalStateException("Value cannot be converted to timestamp: " + this);
223239
}
240+
case UUID -> {
241+
// Compare UUIDs as String representations
242+
// Rationale: @see https://devblogs.microsoft.com/oldnewthing/20190913-00/?p=102859
243+
if (isUuid()) {
244+
return uuid.toString();
245+
} else if (isString()) {
246+
try {
247+
UUID.fromString(str);
248+
return str;
249+
} catch (IllegalArgumentException ignored) {
250+
// ...no-op here because we will throw IllegalStateException right after the try() and if (isString())
251+
}
252+
}
253+
throw new IllegalStateException("Value cannot be converted to UUID: " + this);
254+
}
224255
case BOOLEAN -> {
225256
Preconditions.checkState(isBool(), "Value is not a boolean: %s", this);
226257
return bool;
@@ -252,8 +283,14 @@ public String toString() {
252283
return bool.toString();
253284
} else if (isTimestamp()) {
254285
return "#" + timestamp + "#";
255-
} else {
286+
} else if (isByteArray()) {
287+
return byteArray.toString();
288+
} else if (isTuple()) {
256289
return tuple.toString();
290+
} else if (isUuid()) {
291+
return "uuid(" + uuid + ")";
292+
} else {
293+
return "???";
257294
}
258295
}
259296

@@ -272,7 +309,9 @@ public boolean equals(Object o) {
272309
&& Objects.equals(bool, that.bool)
273310
&& Objects.equals(timestamp, that.timestamp)
274311
&& Objects.equals(real, that.real)
275-
&& Objects.equals(tuple, that.tuple);
312+
&& Objects.equals(tuple, that.tuple)
313+
&& Objects.equals(byteArray, that.byteArray)
314+
&& Objects.equals(uuid, that.uuid);
276315
}
277316

278317
@Override
@@ -291,18 +330,52 @@ public int hashCode() {
291330
if (tuple != null) {
292331
result = result * 59 + tuple.hashCode();
293332
}
333+
if (byteArray != null) {
334+
result = result * 59 + byteArray.hashCode();
335+
}
336+
if (uuid != null) {
337+
result = result * 59 + uuid.hashCode();
338+
}
294339

295340
return result;
296341
}
297342

343+
public record FieldAndValue(
344+
@NonNull JavaField field,
345+
@Nullable FieldValue value
346+
) {
347+
public FieldAndValue(@NonNull JavaField jf, @NonNull Map<String, Object> flattenedObj) {
348+
this(jf, getValue(jf, flattenedObj));
349+
}
350+
351+
@Nullable
352+
private static FieldValue getValue(@NonNull JavaField jf, @NonNull Map<String, Object> flattenedObj) {
353+
String name = jf.getName();
354+
return flattenedObj.containsKey(name) ? FieldValue.ofObj(flattenedObj.get(name), jf) : null;
355+
}
356+
357+
@Nullable
358+
public Comparable<?> toComparable() {
359+
return value == null ? null : value.getComparable(field);
360+
}
361+
362+
public Type fieldType() {
363+
return field.getType();
364+
}
365+
366+
public String fieldPath() {
367+
return field.getPath();
368+
}
369+
}
370+
298371
@Value
299372
public static class Tuple implements Comparable<Tuple> {
300373
@Nullable
301374
@EqualsAndHashCode.Exclude
302375
Object composite;
303376

304377
@NonNull
305-
List<JavaFieldValue> components;
378+
List<FieldAndValue> components;
306379

307380
@NonNull
308381
public Type getType() {
@@ -317,13 +390,13 @@ public Object asComposite() {
317390
}
318391

319392
@NonNull
320-
public Stream<JavaFieldValue> streamComponents() {
393+
public Stream<FieldAndValue> streamComponents() {
321394
return components.stream();
322395
}
323396

324397
@NonNull
325398
public String toString() {
326-
return components.stream().map(c -> String.valueOf(c.getValue())).collect(joining(", ", "<", ">"));
399+
return components.stream().map(fv -> String.valueOf(fv.value())).collect(joining(", ", "<", ">"));
327400
}
328401

329402
@Override
@@ -340,11 +413,11 @@ public int compareTo(@NonNull FieldValue.Tuple other) {
340413
var thisIter = components.iterator();
341414
var otherIter = other.components.iterator();
342415
while (thisIter.hasNext()) {
343-
JavaFieldValue thisComponent = thisIter.next();
344-
JavaFieldValue otherComponent = otherIter.next();
416+
FieldAndValue thisComponent = thisIter.next();
417+
FieldAndValue otherComponent = otherIter.next();
345418

346-
Object thisValue = thisComponent.getValue();
347-
Object otherValue = otherComponent.getValue();
419+
Comparable<?> thisValue = thisComponent.toComparable();
420+
Comparable<?> otherValue = otherComponent.toComparable();
348421
// sort null first
349422
if (thisValue == null && otherValue == null) {
350423
continue;
@@ -357,9 +430,9 @@ public int compareTo(@NonNull FieldValue.Tuple other) {
357430
}
358431

359432
Preconditions.checkState(
360-
thisComponent.getFieldType().equals(otherComponent.getFieldType()),
433+
thisComponent.fieldType().equals(otherComponent.fieldType()),
361434
"Different tuple component types at [%s](%s): %s and %s",
362-
i, thisComponent.getFieldPath(), thisComponent.getFieldType(), otherComponent.getFieldType()
435+
i, thisComponent.fieldPath(), thisComponent.fieldType(), otherComponent.fieldType()
363436
);
364437

365438
@SuppressWarnings({"rawtypes", "unchecked"})

databind/src/main/java/tech/ydb/yoj/databind/expression/IllegalExpressionException.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,11 @@ static final class DateTimeFieldExpected extends FieldTypeError {
6767
super(field, "Type mismatch: cannot compare field \"%s\" with a date-time value"::formatted);
6868
}
6969
}
70+
71+
static final class UuidFieldExpected extends FieldTypeError {
72+
UuidFieldExpected(String field) {
73+
super(field, "Type mismatch: cannot compare field \"%s\" with an UUID value"::formatted);
74+
}
75+
}
7076
}
7177
}

0 commit comments

Comments
 (0)