1111import tech .ydb .yoj .databind .FieldValueType ;
1212import tech .ydb .yoj .databind .schema .ObjectSchema ;
1313import tech .ydb .yoj .databind .schema .Schema .JavaField ;
14- import tech .ydb .yoj .databind .schema .Schema .JavaFieldValue ;
1514
1615import javax .annotation .Nullable ;
1716import java .lang .reflect .Type ;
1817import java .time .Instant ;
18+ import java .util .ArrayList ;
1919import java .util .Collections ;
2020import java .util .List ;
2121import java .util .Map ;
2222import java .util .Objects ;
23+ import java .util .UUID ;
2324import java .util .stream .Stream ;
2425
2526import static java .util .stream .Collectors .collectingAndThen ;
2627import static java .util .stream .Collectors .joining ;
27- import static java .util .stream .Collectors .toList ;
28+ import static java .util .stream .Collectors .toCollection ;
2829import 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" })
0 commit comments