From 4d5003a9f1222626aa2197dd794e8c5ee572ba1a Mon Sep 17 00:00:00 2001 From: Maksim Zinal Date: Wed, 30 Jul 2025 23:04:46 +0300 Subject: [PATCH 1/4] basic comparison logic --- .../tech/ydb/table/values/DecimalValue.java | 59 ++++ .../java/tech/ydb/table/values/DictValue.java | 83 +++++ .../java/tech/ydb/table/values/ListValue.java | 52 +++ .../java/tech/ydb/table/values/NullValue.java | 14 + .../tech/ydb/table/values/OptionalValue.java | 58 ++++ .../tech/ydb/table/values/PrimitiveValue.java | 80 +++++ .../tech/ydb/table/values/StructValue.java | 52 +++ .../tech/ydb/table/values/TupleValue.java | 52 +++ .../java/tech/ydb/table/values/Value.java | 14 +- .../tech/ydb/table/values/VariantValue.java | 45 +++ .../java/tech/ydb/table/values/VoidValue.java | 14 + .../ydb/table/values/ValueComparableTest.java | 295 ++++++++++++++++++ 12 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 table/src/test/java/tech/ydb/table/values/ValueComparableTest.java diff --git a/table/src/main/java/tech/ydb/table/values/DecimalValue.java b/table/src/main/java/tech/ydb/table/values/DecimalValue.java index bc68308f..ea48ca85 100644 --- a/table/src/main/java/tech/ydb/table/values/DecimalValue.java +++ b/table/src/main/java/tech/ydb/table/values/DecimalValue.java @@ -269,6 +269,65 @@ public ValueProtos.Value toPb() { return ProtoValue.fromDecimal(high, low); } + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof DecimalValue)) { + throw new IllegalArgumentException("Cannot compare DecimalValue with " + other.getClass().getSimpleName()); + } + + DecimalValue otherDecimal = (DecimalValue) other; + + // Handle special values first + if (isNan() || otherDecimal.isNan()) { + // NaN is not comparable, but we need to provide a consistent ordering + if (isNan() && otherDecimal.isNan()) { + return 0; + } + if (isNan()) { + return 1; // NaN is considered greater than any other value + } + return -1; + } + + if (isInf() && otherDecimal.isInf()) { + return 0; + } + if (isInf()) { + return 1; // Positive infinity is greater than any finite value + } + if (otherDecimal.isInf()) { + return -1; + } + + if (isNegativeInf() && otherDecimal.isNegativeInf()) { + return 0; + } + if (isNegativeInf()) { + return -1; // Negative infinity is less than any finite value + } + if (otherDecimal.isNegativeInf()) { + return 1; + } + + // Compare finite values + if (isNegative() != otherDecimal.isNegative()) { + return isNegative() ? -1 : 1; + } + + // Both have the same sign, compare magnitudes + int highComparison = Long.compareUnsigned(high, otherDecimal.high); + if (highComparison != 0) { + return isNegative() ? -highComparison : highComparison; + } + + int lowComparison = Long.compareUnsigned(low, otherDecimal.low); + return isNegative() ? -lowComparison : lowComparison; + } + /** * Write long to a big-endian buffer. */ diff --git a/table/src/main/java/tech/ydb/table/values/DictValue.java b/table/src/main/java/tech/ydb/table/values/DictValue.java index 59c9d28b..6ba9db07 100644 --- a/table/src/main/java/tech/ydb/table/values/DictValue.java +++ b/table/src/main/java/tech/ydb/table/values/DictValue.java @@ -1,7 +1,9 @@ package tech.ydb.table.values; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -116,4 +118,85 @@ public ValueProtos.Value toPb() { } return builder.build(); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof DictValue)) { + throw new IllegalArgumentException("Cannot compare DictValue with " + other.getClass().getSimpleName()); + } + + DictValue otherDict = (DictValue) other; + + // Compare sizes first + int sizeComparison = Integer.compare(items.size(), otherDict.items.size()); + if (sizeComparison != 0) { + return sizeComparison; + } + + // Convert to sorted lists for lexicographical comparison + List, Value>> thisEntries = new ArrayList<>(items.entrySet()); + List, Value>> otherEntries = new ArrayList<>(otherDict.items.entrySet()); + + // Sort entries by key first, then by value + thisEntries.sort((e1, e2) -> { + int keyComparison = compareValues(e1.getKey(), e2.getKey()); + if (keyComparison != 0) { + return keyComparison; + } + return compareValues(e1.getValue(), e2.getValue()); + }); + + otherEntries.sort((e1, e2) -> { + int keyComparison = compareValues(e1.getKey(), e2.getKey()); + if (keyComparison != 0) { + return keyComparison; + } + return compareValues(e1.getValue(), e2.getValue()); + }); + + // Compare sorted entries + for (int i = 0; i < thisEntries.size(); i++) { + Map.Entry, Value> thisEntry = thisEntries.get(i); + Map.Entry, Value> otherEntry = otherEntries.get(i); + + int keyComparison = compareValues(thisEntry.getKey(), otherEntry.getKey()); + if (keyComparison != 0) { + return keyComparison; + } + + int valueComparison = compareValues(thisEntry.getValue(), otherEntry.getValue()); + if (valueComparison != 0) { + return valueComparison; + } + } + + return 0; + } + + private static int compareValues(Value a, Value b) { + // Handle null values + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + + // Check that the types are the same + if (!a.getType().equals(b.getType())) { + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getType() + " vs " + b.getType()); + } + + // Use the actual compareTo method of the values + if (a instanceof Comparable && b instanceof Comparable) { + try { + return ((Comparable>) a).compareTo((Value) b); + } catch (ClassCastException e) {} + } + + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + } } diff --git a/table/src/main/java/tech/ydb/table/values/ListValue.java b/table/src/main/java/tech/ydb/table/values/ListValue.java index 94699b60..38a11703 100644 --- a/table/src/main/java/tech/ydb/table/values/ListValue.java +++ b/table/src/main/java/tech/ydb/table/values/ListValue.java @@ -113,4 +113,56 @@ public ValueProtos.Value toPb() { } return builder.build(); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof ListValue)) { + throw new IllegalArgumentException("Cannot compare ListValue with " + other.getClass().getSimpleName()); + } + + ListValue otherList = (ListValue) other; + + // Compare elements lexicographically + int minLength = Math.min(items.length, otherList.items.length); + for (int i = 0; i < minLength; i++) { + Value thisItem = items[i]; + Value otherItem = otherList.items[i]; + + int itemComparison = compareValues(thisItem, otherItem); + if (itemComparison != 0) { + return itemComparison; + } + } + + // If we reach here, one list is a prefix of the other + // The shorter list comes first + return Integer.compare(items.length, otherList.items.length); + } + + private static int compareValues(Value a, Value b) { + // Handle null values + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + + // Check that the types are the same + if (!a.getType().equals(b.getType())) { + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getType() + " vs " + b.getType()); + } + + // Use the actual compareTo method of the values + if (a instanceof Comparable && b instanceof Comparable) { + try { + return ((Comparable>) a).compareTo((Value) b); + } catch (ClassCastException e) {} + } + + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + } } diff --git a/table/src/main/java/tech/ydb/table/values/NullValue.java b/table/src/main/java/tech/ydb/table/values/NullValue.java index 8d363796..352ef1ba 100644 --- a/table/src/main/java/tech/ydb/table/values/NullValue.java +++ b/table/src/main/java/tech/ydb/table/values/NullValue.java @@ -32,4 +32,18 @@ public NullType getType() { public ValueProtos.Value toPb() { return ProtoValue.nullValue(); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof NullValue)) { + throw new IllegalArgumentException("Cannot compare NullValue with " + other.getClass().getSimpleName()); + } + + // All NullValue instances are equal + return 0; + } } diff --git a/table/src/main/java/tech/ydb/table/values/OptionalValue.java b/table/src/main/java/tech/ydb/table/values/OptionalValue.java index 71c8d3f1..8d31dc69 100644 --- a/table/src/main/java/tech/ydb/table/values/OptionalValue.java +++ b/table/src/main/java/tech/ydb/table/values/OptionalValue.java @@ -96,4 +96,62 @@ public ValueProtos.Value toPb() { return ProtoValue.optional(); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + // Handle comparison with another OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item types are the same + if (!type.getItemType().equals(otherOptional.type.getItemType())) { + throw new IllegalArgumentException("Cannot compare OptionalValue with different item types: " + + type.getItemType() + " vs " + otherOptional.type.getItemType()); + } + + // Handle empty values: empty values are considered less than non-empty values + if (value == null && otherOptional.value == null) { + return 0; + } + if (value == null) { + return -1; + } + if (otherOptional.value == null) { + return 1; + } + + // Both values are non-null and have the same type, compare them using their compareTo method + return compareValues(value, otherOptional.value); + } + + // Handle comparison with non-optional values of the same underlying type + if (type.getItemType().equals(other.getType())) { + // This OptionalValue is empty, so it's less than any non-optional value + if (value == null) { + return -1; + } + + // This OptionalValue has a value, compare it with the non-optional value + return compareValues(value, other); + } + + // Types are incompatible + throw new IllegalArgumentException("Cannot compare OptionalValue with incompatible type: " + + type.getItemType() + " vs " + other.getType()); + } + + private static int compareValues(Value a, Value b) { + // Since we've already verified the types are the same, we can safely cast + // and use the compareTo method of the actual value type + if (a instanceof Comparable && b instanceof Comparable) { + try { + return ((Comparable>) a).compareTo((Value) b); + } catch (ClassCastException e) {} + } + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + } } diff --git a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java index 10338715..1c61c84f 100644 --- a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java +++ b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java @@ -423,6 +423,86 @@ private static void checkType(PrimitiveType expected, PrimitiveType actual) { } } + private static int compareByteArrays(byte[] a, byte[] b) { + int minLength = Math.min(a.length, b.length); + for (int i = 0; i < minLength; i++) { + int comparison = Byte.compare(a[i], b[i]); + if (comparison != 0) { + return comparison; + } + } + return Integer.compare(a.length, b.length); + } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof PrimitiveValue)) { + throw new IllegalArgumentException("Cannot compare PrimitiveValue with " + other.getClass().getSimpleName()); + } + + PrimitiveValue otherPrimitive = (PrimitiveValue) other; + if (getType() != otherPrimitive.getType()) { + throw new IllegalArgumentException("Cannot compare values of different types: " + getType() + " vs " + otherPrimitive.getType()); + } + + // Compare based on the actual primitive type + switch (getType()) { + case Bool: + return Boolean.compare(getBool(), otherPrimitive.getBool()); + case Int8: + return Byte.compare(getInt8(), otherPrimitive.getInt8()); + case Uint8: + return Integer.compare(getUint8(), otherPrimitive.getUint8()); + case Int16: + return Short.compare(getInt16(), otherPrimitive.getInt16()); + case Uint16: + return Integer.compare(getUint16(), otherPrimitive.getUint16()); + case Int32: + return Integer.compare(getInt32(), otherPrimitive.getInt32()); + case Uint32: + return Long.compare(getUint32(), otherPrimitive.getUint32()); + case Int64: + return Long.compare(getInt64(), otherPrimitive.getInt64()); + case Uint64: + return Long.compare(getUint64(), otherPrimitive.getUint64()); + case Float: + return Float.compare(getFloat(), otherPrimitive.getFloat()); + case Double: + return Double.compare(getDouble(), otherPrimitive.getDouble()); + case Bytes: + case Yson: + return compareByteArrays(getBytesUnsafe(), otherPrimitive.getBytesUnsafe()); + case Text: + case Json: + case JsonDocument: + return getText().compareTo(otherPrimitive.getText()); + case Uuid: + return getUuidJdk().compareTo(otherPrimitive.getUuidJdk()); + case Date: + case Date32: + return getDate().compareTo(otherPrimitive.getDate()); + case Datetime: + case Datetime64: + return getDatetime().compareTo(otherPrimitive.getDatetime()); + case Timestamp: + case Timestamp64: + return getTimestamp().compareTo(otherPrimitive.getTimestamp()); + case Interval: + case Interval64: + return getInterval().compareTo(otherPrimitive.getInterval()); + case TzDate: + case TzDatetime: + case TzTimestamp: + return getTzDate().compareTo(otherPrimitive.getTzDate()); + default: + throw new UnsupportedOperationException("Comparison not supported for type: " + getType()); + } + } + // -- implementations -- private static final class Bool extends PrimitiveValue { diff --git a/table/src/main/java/tech/ydb/table/values/StructValue.java b/table/src/main/java/tech/ydb/table/values/StructValue.java index 7c34d37a..5b3f44b2 100644 --- a/table/src/main/java/tech/ydb/table/values/StructValue.java +++ b/table/src/main/java/tech/ydb/table/values/StructValue.java @@ -134,6 +134,58 @@ public ValueProtos.Value toPb() { return builder.build(); } + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof StructValue)) { + throw new IllegalArgumentException("Cannot compare StructValue with " + other.getClass().getSimpleName()); + } + + StructValue otherStruct = (StructValue) other; + + // Compare members lexicographically + int minLength = Math.min(members.length, otherStruct.members.length); + for (int i = 0; i < minLength; i++) { + Value thisMember = members[i]; + Value otherMember = otherStruct.members[i]; + + int memberComparison = compareValues(thisMember, otherMember); + if (memberComparison != 0) { + return memberComparison; + } + } + + // If we reach here, one struct is a prefix of the other + // The shorter struct comes first + return Integer.compare(members.length, otherStruct.members.length); + } + + private static int compareValues(Value a, Value b) { + // Handle null values + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + + // Check that the types are the same + if (!a.getType().equals(b.getType())) { + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getType() + " vs " + b.getType()); + } + + // Use the actual compareTo method of the values + if (a instanceof Comparable && b instanceof Comparable) { + try { + return ((Comparable>) a).compareTo((Value) b); + } catch (ClassCastException e) {} + } + + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + } + private static StructValue newStruct(String[] names, Value[] values) { Arrays2.sortBothByFirst(names, values); final Type[] types = new Type[values.length]; diff --git a/table/src/main/java/tech/ydb/table/values/TupleValue.java b/table/src/main/java/tech/ydb/table/values/TupleValue.java index 9b80a1b2..f2a6f194 100644 --- a/table/src/main/java/tech/ydb/table/values/TupleValue.java +++ b/table/src/main/java/tech/ydb/table/values/TupleValue.java @@ -145,4 +145,56 @@ private static TupleValue fromArray(Value... items) { } return new TupleValue(TupleType.ofOwn(types), items); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof TupleValue)) { + throw new IllegalArgumentException("Cannot compare TupleValue with " + other.getClass().getSimpleName()); + } + + TupleValue otherTuple = (TupleValue) other; + + // Compare elements lexicographically + int minLength = Math.min(items.length, otherTuple.items.length); + for (int i = 0; i < minLength; i++) { + Value thisItem = items[i]; + Value otherItem = otherTuple.items[i]; + + int itemComparison = compareValues(thisItem, otherItem); + if (itemComparison != 0) { + return itemComparison; + } + } + + // If we reach here, one tuple is a prefix of the other + // The shorter tuple comes first + return Integer.compare(items.length, otherTuple.items.length); + } + + private static int compareValues(Value a, Value b) { + // Handle null values + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + + // Check that the types are the same + if (!a.getType().equals(b.getType())) { + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getType() + " vs " + b.getType()); + } + + // Use the actual compareTo method of the values + if (a instanceof Comparable && b instanceof Comparable) { + try { + return ((Comparable>) a).compareTo((Value) b); + } catch (ClassCastException e) {} + } + + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + } } diff --git a/table/src/main/java/tech/ydb/table/values/Value.java b/table/src/main/java/tech/ydb/table/values/Value.java index 91ed8df7..a20a41df 100644 --- a/table/src/main/java/tech/ydb/table/values/Value.java +++ b/table/src/main/java/tech/ydb/table/values/Value.java @@ -8,7 +8,7 @@ * @author Sergey Polovko * @param type of value */ -public interface Value extends Serializable { +public interface Value extends Serializable, Comparable> { Value[] EMPTY_ARRAY = {}; @@ -16,6 +16,18 @@ public interface Value extends Serializable { ValueProtos.Value toPb(); + /** + * Compares this value with another value. + * The comparison is based on the actual data type of the value stored. + * For complex types like ListValue and StructValue, the comparison follows lexicographical rules. + * For OptionalValue, comparison with non-optional values of the same underlying type is supported. + * + * @param other the value to compare with + * @return a negative integer, zero, or a positive integer as this value is less than, equal to, or greater than the other value + * @throws IllegalArgumentException if the other value is null or has an incompatible type + */ + int compareTo(Value other); + default PrimitiveValue asData() { return (PrimitiveValue) this; } diff --git a/table/src/main/java/tech/ydb/table/values/VariantValue.java b/table/src/main/java/tech/ydb/table/values/VariantValue.java index a8fe231b..d2f700b8 100644 --- a/table/src/main/java/tech/ydb/table/values/VariantValue.java +++ b/table/src/main/java/tech/ydb/table/values/VariantValue.java @@ -70,4 +70,49 @@ public ValueProtos.Value toPb() { builder.setVariantIndex(typeIndex); return builder.build(); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof VariantValue)) { + throw new IllegalArgumentException("Cannot compare VariantValue with " + other.getClass().getSimpleName()); + } + + VariantValue otherVariant = (VariantValue) other; + + // Compare type indices first + int indexComparison = Integer.compare(typeIndex, otherVariant.typeIndex); + if (indexComparison != 0) { + return indexComparison; + } + + // If type indices are the same, compare the items + return compareValues(item, otherVariant.item); + } + + private static int compareValues(Value a, Value b) { + // Handle null values + if (a == null && b == null) return 0; + if (a == null) return -1; + if (b == null) return 1; + + // Check that the types are the same + if (!a.getType().equals(b.getType())) { + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getType() + " vs " + b.getType()); + } + + // Use the actual compareTo method of the values + if (a instanceof Comparable && b instanceof Comparable) { + try { + return ((Comparable>) a).compareTo((Value) b); + } catch (ClassCastException e) {} + } + + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + } } diff --git a/table/src/main/java/tech/ydb/table/values/VoidValue.java b/table/src/main/java/tech/ydb/table/values/VoidValue.java index ba96c879..0e223953 100644 --- a/table/src/main/java/tech/ydb/table/values/VoidValue.java +++ b/table/src/main/java/tech/ydb/table/values/VoidValue.java @@ -42,4 +42,18 @@ public VoidType getType() { public ValueProtos.Value toPb() { return ProtoValue.voidValue(); } + + @Override + public int compareTo(Value other) { + if (other == null) { + throw new IllegalArgumentException("Cannot compare with null value"); + } + + if (!(other instanceof VoidValue)) { + throw new IllegalArgumentException("Cannot compare VoidValue with " + other.getClass().getSimpleName()); + } + + // All VoidValue instances are equal + return 0; + } } diff --git a/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java new file mode 100644 index 00000000..9ff82e04 --- /dev/null +++ b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java @@ -0,0 +1,295 @@ +package tech.ydb.table.values; + +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.List; + +/** + * Test for Comparable implementation of Value classes + */ +public class ValueComparableTest { + + @Test + public void testPrimitiveValueComparison() { + // Test numeric comparisons + PrimitiveValue int1 = PrimitiveValue.newInt32(1); + PrimitiveValue int2 = PrimitiveValue.newInt32(2); + PrimitiveValue int3 = PrimitiveValue.newInt32(1); + + assertTrue(int1.compareTo(int2) < 0); + assertTrue(int2.compareTo(int1) > 0); + assertEquals(0, int1.compareTo(int3)); + + // Test string comparisons + PrimitiveValue text1 = PrimitiveValue.newText("abc"); + PrimitiveValue text2 = PrimitiveValue.newText("def"); + PrimitiveValue text3 = PrimitiveValue.newText("abc"); + + assertTrue(text1.compareTo(text2) < 0); + assertTrue(text2.compareTo(text1) > 0); + assertEquals(0, text1.compareTo(text3)); + + // Test boolean comparisons + PrimitiveValue bool1 = PrimitiveValue.newBool(false); + PrimitiveValue bool2 = PrimitiveValue.newBool(true); + + assertTrue(bool1.compareTo(bool2) < 0); + assertTrue(bool2.compareTo(bool1) > 0); + } + + @Test + public void testListValueComparison() { + ListValue list1 = ListValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(2)); + ListValue list2 = ListValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(3)); + ListValue list3 = ListValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(2)); + ListValue list4 = ListValue.of(PrimitiveValue.newInt32(1)); + + assertTrue(list1.compareTo(list2) < 0); + assertTrue(list2.compareTo(list1) > 0); + assertEquals(0, list1.compareTo(list3)); + assertTrue(list4.compareTo(list1) < 0); // shorter list comes first + } + + @Test + public void testListValueLexicographical() { + // Test proper lexicographical ordering + ListValue list1 = ListValue.of(PrimitiveValue.newText("A"), PrimitiveValue.newText("Z")); + ListValue list2 = ListValue.of(PrimitiveValue.newText("Z")); + + // ('Z') should be "bigger" than ('A','Z') in lexicographical order + assertTrue(list1.compareTo(list2) < 0); // ('A','Z') < ('Z') + assertTrue(list2.compareTo(list1) > 0); // ('Z') > ('A','Z') + + // Test prefix ordering + ListValue list3 = ListValue.of(PrimitiveValue.newText("A")); + ListValue list4 = ListValue.of(PrimitiveValue.newText("A"), PrimitiveValue.newText("B")); + + assertTrue(list3.compareTo(list4) < 0); // ('A') < ('A','B') + assertTrue(list4.compareTo(list3) > 0); // ('A','B') > ('A') + } + + @Test(expected = IllegalArgumentException.class) + public void testListValueDifferentTypes() { + ListValue list1 = ListValue.of(PrimitiveValue.newInt32(1)); + ListValue list2 = ListValue.of(PrimitiveValue.newText("abc")); + list1.compareTo(list2); // Should throw exception for different element types + } + + @Test + public void testStructValueComparison() { + StructValue struct1 = StructValue.of("a", PrimitiveValue.newInt32(1), "b", PrimitiveValue.newInt32(2)); + StructValue struct2 = StructValue.of("a", PrimitiveValue.newInt32(1), "b", PrimitiveValue.newInt32(3)); + StructValue struct3 = StructValue.of("a", PrimitiveValue.newInt32(1), "b", PrimitiveValue.newInt32(2)); + + assertTrue(struct1.compareTo(struct2) < 0); + assertTrue(struct2.compareTo(struct1) > 0); + assertEquals(0, struct1.compareTo(struct3)); + } + + @Test + public void testStructValueLexicographical() { + // Test proper lexicographical ordering + StructValue struct1 = StructValue.of("a", PrimitiveValue.newText("A"), "b", PrimitiveValue.newText("Z")); + StructValue struct2 = StructValue.of("a", PrimitiveValue.newText("Z")); + + // ('Z') should be "bigger" than ('A','Z') in lexicographical order + assertTrue(struct1.compareTo(struct2) < 0); // ('A','Z') < ('Z') + assertTrue(struct2.compareTo(struct1) > 0); // ('Z') > ('A','Z') + } + + @Test(expected = IllegalArgumentException.class) + public void testStructValueDifferentTypes() { + StructValue struct1 = StructValue.of("a", PrimitiveValue.newInt32(1)); + StructValue struct2 = StructValue.of("a", PrimitiveValue.newText("abc")); + struct1.compareTo(struct2); // Should throw exception for different member types + } + + @Test + public void testDictValueComparison() { + DictValue dict1 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + DictValue dict2 = DictValue.of(PrimitiveValue.newText("b"), PrimitiveValue.newInt32(1)); + DictValue dict3 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + + assertTrue(dict1.compareTo(dict2) < 0); + assertTrue(dict2.compareTo(dict1) > 0); + assertEquals(0, dict1.compareTo(dict3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testDictValueDifferentTypes() { + DictValue dict1 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + DictValue dict2 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newText("abc")); + dict1.compareTo(dict2); // Should throw exception for different value types + } + + @Test + public void testOptionalValueComparison() { + OptionalValue opt1 = OptionalValue.of(PrimitiveValue.newInt32(1)); + OptionalValue opt2 = OptionalValue.of(PrimitiveValue.newInt32(2)); + OptionalValue opt3 = OptionalValue.of(PrimitiveValue.newInt32(1)); + OptionalValue opt4 = OptionalValue.of(null); + + assertTrue(opt1.compareTo(opt2) < 0); + assertTrue(opt2.compareTo(opt1) > 0); + assertEquals(0, opt1.compareTo(opt3)); + assertTrue(opt4.compareTo(opt1) < 0); // empty values come first + } + + @Test(expected = IllegalArgumentException.class) + public void testOptionalValueDifferentTypes() { + OptionalValue opt1 = OptionalValue.of(PrimitiveValue.newInt32(1)); + OptionalValue opt2 = OptionalValue.of(PrimitiveValue.newText("abc")); + opt1.compareTo(opt2); // Should throw exception for different item types + } + + @Test + public void testOptionalValueWithNonOptional() { + OptionalValue opt1 = OptionalValue.of(PrimitiveValue.newInt32(1)); + OptionalValue opt2 = OptionalValue.of(PrimitiveValue.newInt32(2)); + OptionalValue opt3 = PrimitiveType.Int32.makeOptional().emptyValue(); + PrimitiveValue prim1 = PrimitiveValue.newInt32(1); + PrimitiveValue prim2 = PrimitiveValue.newInt32(2); + + // Optional with non-optional of same type + assertEquals(0, opt1.compareTo(prim1)); // Same value + assertTrue(opt1.compareTo(prim2) < 0); // Optional value less than non-optional + assertTrue(opt2.compareTo(prim1) > 0); // Optional value greater than non-optional + + // Empty optional with non-optional + assertTrue(opt3.compareTo(prim1) < 0); // Empty < non-empty + + // Non-optional with optional + assertEquals(0, prim1.compareTo(opt1)); // Same value + assertTrue(prim1.compareTo(opt2) < 0); // Non-optional less than optional + assertTrue(prim2.compareTo(opt1) > 0); // Non-optional greater than optional + assertTrue(prim1.compareTo(opt3) > 0); // Non-empty > empty + } + + @Test(expected = IllegalArgumentException.class) + public void testOptionalValueWithIncompatibleType() { + OptionalValue opt1 = OptionalValue.of(PrimitiveValue.newInt32(1)); + PrimitiveValue prim1 = PrimitiveValue.newText("abc"); + opt1.compareTo(prim1); // Should throw exception for incompatible types + } + + @Test + public void testTupleValueComparison() { + TupleValue tuple1 = TupleValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(2)); + TupleValue tuple2 = TupleValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(3)); + TupleValue tuple3 = TupleValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(2)); + TupleValue tuple4 = TupleValue.of(PrimitiveValue.newInt32(1)); + + assertTrue(tuple1.compareTo(tuple2) < 0); + assertTrue(tuple2.compareTo(tuple1) > 0); + assertEquals(0, tuple1.compareTo(tuple3)); + assertTrue(tuple4.compareTo(tuple1) < 0); // shorter tuple comes first + } + + @Test + public void testTupleValueLexicographical() { + // Test proper lexicographical ordering + TupleValue tuple1 = TupleValue.of(PrimitiveValue.newText("A"), PrimitiveValue.newText("Z")); + TupleValue tuple2 = TupleValue.of(PrimitiveValue.newText("Z")); + + // ('Z') should be "bigger" than ('A','Z') in lexicographical order + assertTrue(tuple1.compareTo(tuple2) < 0); // ('A','Z') < ('Z') + assertTrue(tuple2.compareTo(tuple1) > 0); // ('Z') > ('A','Z') + } + + @Test(expected = IllegalArgumentException.class) + public void testTupleValueDifferentTypes() { + TupleValue tuple1 = TupleValue.of(PrimitiveValue.newInt32(1)); + TupleValue tuple2 = TupleValue.of(PrimitiveValue.newText("abc")); + tuple1.compareTo(tuple2); // Should throw exception for different element types + } + + @Test + public void testVariantValueComparison() { + VariantValue variant1 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + PrimitiveValue.newInt32(1), 0); + VariantValue variant2 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + PrimitiveValue.newText("abc"), 1); + VariantValue variant3 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + PrimitiveValue.newInt32(2), 0); + + assertTrue(variant1.compareTo(variant2) < 0); // type index 0 < 1 + assertTrue(variant2.compareTo(variant1) > 0); + assertTrue(variant1.compareTo(variant3) < 0); // same type index, compare values + } + + @Test(expected = IllegalArgumentException.class) + public void testVariantValueDifferentTypes() { + VariantValue variant1 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + PrimitiveValue.newInt32(1), 0); + VariantValue variant2 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + PrimitiveValue.newText("abc"), 0); + variant1.compareTo(variant2); // Should throw exception for different item types + } + + @Test + public void testVoidValueComparison() { + VoidValue void1 = VoidValue.of(); + VoidValue void2 = VoidValue.of(); + + assertEquals(0, void1.compareTo(void2)); + } + + @Test + public void testNullValueComparison() { + NullValue null1 = NullValue.of(); + NullValue null2 = NullValue.of(); + + assertEquals(0, null1.compareTo(null2)); + } + + @Test + public void testDecimalValueComparison() { + DecimalValue decimal1 = DecimalValue.fromLong(DecimalType.of(10, 2), 100); + DecimalValue decimal2 = DecimalValue.fromLong(DecimalType.of(10, 2), 200); + DecimalValue decimal3 = DecimalValue.fromLong(DecimalType.of(10, 2), 100); + + assertTrue(decimal1.compareTo(decimal2) < 0); + assertTrue(decimal2.compareTo(decimal1) > 0); + assertEquals(0, decimal1.compareTo(decimal3)); + } + + @Test(expected = IllegalArgumentException.class) + public void testCompareWithNull() { + PrimitiveValue value = PrimitiveValue.newInt32(1); + value.compareTo(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testCompareDifferentTypes() { + PrimitiveValue intValue = PrimitiveValue.newInt32(1); + PrimitiveValue textValue = PrimitiveValue.newText("abc"); + intValue.compareTo(textValue); + } + + @Test + public void testSorting() { + List> values = Arrays.asList( + PrimitiveValue.newInt32(3), + PrimitiveValue.newInt32(1), + PrimitiveValue.newInt32(2), + PrimitiveValue.newText("abc"), + PrimitiveValue.newText("aaa") + ); + + values.sort((a, b) -> { + // Compare by type kind first, then by value + int typeComparison = a.getType().getKind().compareTo(b.getType().getKind()); + if (typeComparison != 0) { + return typeComparison; + } + return a.toString().compareTo(b.toString()); + }); + + // Verify sorting worked + assertTrue(values.get(0).toString().contains("1")); + assertTrue(values.get(1).toString().contains("2")); + assertTrue(values.get(2).toString().contains("3")); + } +} \ No newline at end of file From c5b7231986236c94e28a48f0f66673db92577ba4 Mon Sep 17 00:00:00 2001 From: Maksim Zinal Date: Wed, 30 Jul 2025 23:27:52 +0300 Subject: [PATCH 2/4] proper tests --- .../tech/ydb/table/values/DecimalValue.java | 36 +++++++--- .../java/tech/ydb/table/values/DictValue.java | 68 +++++++++++++------ .../java/tech/ydb/table/values/ListValue.java | 58 ++++++++++++---- .../java/tech/ydb/table/values/NullValue.java | 24 ++++++- .../tech/ydb/table/values/OptionalValue.java | 27 ++++---- .../tech/ydb/table/values/PrimitiveValue.java | 32 +++++++-- .../tech/ydb/table/values/StructValue.java | 58 ++++++++++++---- .../tech/ydb/table/values/TupleValue.java | 58 ++++++++++++---- .../java/tech/ydb/table/values/Value.java | 2 +- .../tech/ydb/table/values/VariantValue.java | 56 +++++++++++---- .../java/tech/ydb/table/values/VoidValue.java | 24 ++++++- .../ydb/table/values/ValueComparableTest.java | 30 +------- 12 files changed, 335 insertions(+), 138 deletions(-) diff --git a/table/src/main/java/tech/ydb/table/values/DecimalValue.java b/table/src/main/java/tech/ydb/table/values/DecimalValue.java index ea48ca85..41464acf 100644 --- a/table/src/main/java/tech/ydb/table/values/DecimalValue.java +++ b/table/src/main/java/tech/ydb/table/values/DecimalValue.java @@ -274,13 +274,33 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this decimal type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare DecimalValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof DecimalValue)) { throw new IllegalArgumentException("Cannot compare DecimalValue with " + other.getClass().getSimpleName()); } - + DecimalValue otherDecimal = (DecimalValue) other; - + // Handle special values first if (isNan() || otherDecimal.isNan()) { // NaN is not comparable, but we need to provide a consistent ordering @@ -292,7 +312,7 @@ public int compareTo(Value other) { } return -1; } - + if (isInf() && otherDecimal.isInf()) { return 0; } @@ -302,7 +322,7 @@ public int compareTo(Value other) { if (otherDecimal.isInf()) { return -1; } - + if (isNegativeInf() && otherDecimal.isNegativeInf()) { return 0; } @@ -312,18 +332,18 @@ public int compareTo(Value other) { if (otherDecimal.isNegativeInf()) { return 1; } - + // Compare finite values if (isNegative() != otherDecimal.isNegative()) { return isNegative() ? -1 : 1; } - + // Both have the same sign, compare magnitudes int highComparison = Long.compareUnsigned(high, otherDecimal.high); if (highComparison != 0) { return isNegative() ? -highComparison : highComparison; } - + int lowComparison = Long.compareUnsigned(low, otherDecimal.low); return isNegative() ? -lowComparison : lowComparison; } diff --git a/table/src/main/java/tech/ydb/table/values/DictValue.java b/table/src/main/java/tech/ydb/table/values/DictValue.java index 6ba9db07..61fc2037 100644 --- a/table/src/main/java/tech/ydb/table/values/DictValue.java +++ b/table/src/main/java/tech/ydb/table/values/DictValue.java @@ -124,23 +124,43 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this dict type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare DictValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof DictValue)) { throw new IllegalArgumentException("Cannot compare DictValue with " + other.getClass().getSimpleName()); } - + DictValue otherDict = (DictValue) other; - + // Compare sizes first int sizeComparison = Integer.compare(items.size(), otherDict.items.size()); if (sizeComparison != 0) { return sizeComparison; } - + // Convert to sorted lists for lexicographical comparison List, Value>> thisEntries = new ArrayList<>(items.entrySet()); List, Value>> otherEntries = new ArrayList<>(otherDict.items.entrySet()); - + // Sort entries by key first, then by value thisEntries.sort((e1, e2) -> { int keyComparison = compareValues(e1.getKey(), e2.getKey()); @@ -149,7 +169,7 @@ public int compareTo(Value other) { } return compareValues(e1.getValue(), e2.getValue()); }); - + otherEntries.sort((e1, e2) -> { int keyComparison = compareValues(e1.getKey(), e2.getKey()); if (keyComparison != 0) { @@ -157,46 +177,54 @@ public int compareTo(Value other) { } return compareValues(e1.getValue(), e2.getValue()); }); - + // Compare sorted entries for (int i = 0; i < thisEntries.size(); i++) { Map.Entry, Value> thisEntry = thisEntries.get(i); Map.Entry, Value> otherEntry = otherEntries.get(i); - + int keyComparison = compareValues(thisEntry.getKey(), otherEntry.getKey()); if (keyComparison != 0) { return keyComparison; } - + int valueComparison = compareValues(thisEntry.getValue(), otherEntry.getValue()); if (valueComparison != 0) { return valueComparison; } } - + return 0; } - + private static int compareValues(Value a, Value b) { // Handle null values - if (a == null && b == null) return 0; - if (a == null) return -1; - if (b == null) return 1; - + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + // Check that the types are the same if (!a.getType().equals(b.getType())) { - throw new IllegalArgumentException("Cannot compare values of different types: " + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getType() + " vs " + b.getType()); } - + // Use the actual compareTo method of the values if (a instanceof Comparable && b instanceof Comparable) { try { return ((Comparable>) a).compareTo((Value) b); - } catch (ClassCastException e) {} + } catch (ClassCastException e) { + // Fall back to error + } } - - throw new IllegalArgumentException("Cannot compare values of different types: " + + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); } } diff --git a/table/src/main/java/tech/ydb/table/values/ListValue.java b/table/src/main/java/tech/ydb/table/values/ListValue.java index 38a11703..f34f9419 100644 --- a/table/src/main/java/tech/ydb/table/values/ListValue.java +++ b/table/src/main/java/tech/ydb/table/values/ListValue.java @@ -119,50 +119,78 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this list type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare ListValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof ListValue)) { throw new IllegalArgumentException("Cannot compare ListValue with " + other.getClass().getSimpleName()); } - + ListValue otherList = (ListValue) other; - + // Compare elements lexicographically int minLength = Math.min(items.length, otherList.items.length); for (int i = 0; i < minLength; i++) { Value thisItem = items[i]; Value otherItem = otherList.items[i]; - + int itemComparison = compareValues(thisItem, otherItem); if (itemComparison != 0) { return itemComparison; } } - + // If we reach here, one list is a prefix of the other // The shorter list comes first return Integer.compare(items.length, otherList.items.length); } - + private static int compareValues(Value a, Value b) { // Handle null values - if (a == null && b == null) return 0; - if (a == null) return -1; - if (b == null) return 1; - + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + // Check that the types are the same if (!a.getType().equals(b.getType())) { - throw new IllegalArgumentException("Cannot compare values of different types: " + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getType() + " vs " + b.getType()); } - + // Use the actual compareTo method of the values if (a instanceof Comparable && b instanceof Comparable) { try { return ((Comparable>) a).compareTo((Value) b); - } catch (ClassCastException e) {} + } catch (ClassCastException e) { + // Fall back to error + } } - - throw new IllegalArgumentException("Cannot compare values of different types: " + + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); } } diff --git a/table/src/main/java/tech/ydb/table/values/NullValue.java b/table/src/main/java/tech/ydb/table/values/NullValue.java index 352ef1ba..b106688b 100644 --- a/table/src/main/java/tech/ydb/table/values/NullValue.java +++ b/table/src/main/java/tech/ydb/table/values/NullValue.java @@ -38,11 +38,31 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this null type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare NullValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof NullValue)) { throw new IllegalArgumentException("Cannot compare NullValue with " + other.getClass().getSimpleName()); } - + // All NullValue instances are equal return 0; } diff --git a/table/src/main/java/tech/ydb/table/values/OptionalValue.java b/table/src/main/java/tech/ydb/table/values/OptionalValue.java index 8d31dc69..dde545c6 100644 --- a/table/src/main/java/tech/ydb/table/values/OptionalValue.java +++ b/table/src/main/java/tech/ydb/table/values/OptionalValue.java @@ -102,17 +102,17 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + // Handle comparison with another OptionalValue if (other instanceof OptionalValue) { OptionalValue otherOptional = (OptionalValue) other; - + // Check that the item types are the same if (!type.getItemType().equals(otherOptional.type.getItemType())) { - throw new IllegalArgumentException("Cannot compare OptionalValue with different item types: " + + throw new IllegalArgumentException("Cannot compare OptionalValue with different item types: " + type.getItemType() + " vs " + otherOptional.type.getItemType()); } - + // Handle empty values: empty values are considered less than non-empty values if (value == null && otherOptional.value == null) { return 0; @@ -123,35 +123,38 @@ public int compareTo(Value other) { if (otherOptional.value == null) { return 1; } - + // Both values are non-null and have the same type, compare them using their compareTo method return compareValues(value, otherOptional.value); } - + // Handle comparison with non-optional values of the same underlying type if (type.getItemType().equals(other.getType())) { // This OptionalValue is empty, so it's less than any non-optional value if (value == null) { return -1; } - + // This OptionalValue has a value, compare it with the non-optional value return compareValues(value, other); } - + // Types are incompatible - throw new IllegalArgumentException("Cannot compare OptionalValue with incompatible type: " + + throw new IllegalArgumentException("Cannot compare OptionalValue with incompatible type: " + type.getItemType() + " vs " + other.getType()); } - + private static int compareValues(Value a, Value b) { // Since we've already verified the types are the same, we can safely cast // and use the compareTo method of the actual value type if (a instanceof Comparable && b instanceof Comparable) { try { return ((Comparable>) a).compareTo((Value) b); - } catch (ClassCastException e) {} + } catch (ClassCastException e) { + // Fall back to error + } } - throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); + throw new IllegalArgumentException("Cannot compare values of different types: " + + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); } } diff --git a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java index 1c61c84f..9613f1d7 100644 --- a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java +++ b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java @@ -439,16 +439,38 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this primitive type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare PrimitiveValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof PrimitiveValue)) { - throw new IllegalArgumentException("Cannot compare PrimitiveValue with " + other.getClass().getSimpleName()); + throw new IllegalArgumentException( + "Cannot compare PrimitiveValue with " + other.getClass().getSimpleName()); } - + PrimitiveValue otherPrimitive = (PrimitiveValue) other; if (getType() != otherPrimitive.getType()) { - throw new IllegalArgumentException("Cannot compare values of different types: " + getType() + " vs " + otherPrimitive.getType()); + throw new IllegalArgumentException("Cannot compare values of different types: " + + getType() + " vs " + otherPrimitive.getType()); } - + // Compare based on the actual primitive type switch (getType()) { case Bool: diff --git a/table/src/main/java/tech/ydb/table/values/StructValue.java b/table/src/main/java/tech/ydb/table/values/StructValue.java index 5b3f44b2..196771c2 100644 --- a/table/src/main/java/tech/ydb/table/values/StructValue.java +++ b/table/src/main/java/tech/ydb/table/values/StructValue.java @@ -139,50 +139,78 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this struct type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare StructValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof StructValue)) { throw new IllegalArgumentException("Cannot compare StructValue with " + other.getClass().getSimpleName()); } - + StructValue otherStruct = (StructValue) other; - + // Compare members lexicographically int minLength = Math.min(members.length, otherStruct.members.length); for (int i = 0; i < minLength; i++) { Value thisMember = members[i]; Value otherMember = otherStruct.members[i]; - + int memberComparison = compareValues(thisMember, otherMember); if (memberComparison != 0) { return memberComparison; } } - + // If we reach here, one struct is a prefix of the other // The shorter struct comes first return Integer.compare(members.length, otherStruct.members.length); } - + private static int compareValues(Value a, Value b) { // Handle null values - if (a == null && b == null) return 0; - if (a == null) return -1; - if (b == null) return 1; - + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + // Check that the types are the same if (!a.getType().equals(b.getType())) { - throw new IllegalArgumentException("Cannot compare values of different types: " + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getType() + " vs " + b.getType()); } - + // Use the actual compareTo method of the values if (a instanceof Comparable && b instanceof Comparable) { try { return ((Comparable>) a).compareTo((Value) b); - } catch (ClassCastException e) {} + } catch (ClassCastException e) { + // Fall back to error + } } - - throw new IllegalArgumentException("Cannot compare values of different types: " + + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); } diff --git a/table/src/main/java/tech/ydb/table/values/TupleValue.java b/table/src/main/java/tech/ydb/table/values/TupleValue.java index f2a6f194..00f16ed4 100644 --- a/table/src/main/java/tech/ydb/table/values/TupleValue.java +++ b/table/src/main/java/tech/ydb/table/values/TupleValue.java @@ -151,50 +151,78 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this tuple type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare TupleValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof TupleValue)) { throw new IllegalArgumentException("Cannot compare TupleValue with " + other.getClass().getSimpleName()); } - + TupleValue otherTuple = (TupleValue) other; - + // Compare elements lexicographically int minLength = Math.min(items.length, otherTuple.items.length); for (int i = 0; i < minLength; i++) { Value thisItem = items[i]; Value otherItem = otherTuple.items[i]; - + int itemComparison = compareValues(thisItem, otherItem); if (itemComparison != 0) { return itemComparison; } } - + // If we reach here, one tuple is a prefix of the other // The shorter tuple comes first return Integer.compare(items.length, otherTuple.items.length); } - + private static int compareValues(Value a, Value b) { // Handle null values - if (a == null && b == null) return 0; - if (a == null) return -1; - if (b == null) return 1; - + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + // Check that the types are the same if (!a.getType().equals(b.getType())) { - throw new IllegalArgumentException("Cannot compare values of different types: " + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getType() + " vs " + b.getType()); } - + // Use the actual compareTo method of the values if (a instanceof Comparable && b instanceof Comparable) { try { return ((Comparable>) a).compareTo((Value) b); - } catch (ClassCastException e) {} + } catch (ClassCastException e) { + // Fall back to error + } } - - throw new IllegalArgumentException("Cannot compare values of different types: " + + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); } } diff --git a/table/src/main/java/tech/ydb/table/values/Value.java b/table/src/main/java/tech/ydb/table/values/Value.java index a20a41df..d2186708 100644 --- a/table/src/main/java/tech/ydb/table/values/Value.java +++ b/table/src/main/java/tech/ydb/table/values/Value.java @@ -21,7 +21,7 @@ public interface Value extends Serializable, Comparable * The comparison is based on the actual data type of the value stored. * For complex types like ListValue and StructValue, the comparison follows lexicographical rules. * For OptionalValue, comparison with non-optional values of the same underlying type is supported. - * + * * @param other the value to compare with * @return a negative integer, zero, or a positive integer as this value is less than, equal to, or greater than the other value * @throws IllegalArgumentException if the other value is null or has an incompatible type diff --git a/table/src/main/java/tech/ydb/table/values/VariantValue.java b/table/src/main/java/tech/ydb/table/values/VariantValue.java index d2f700b8..dda7842e 100644 --- a/table/src/main/java/tech/ydb/table/values/VariantValue.java +++ b/table/src/main/java/tech/ydb/table/values/VariantValue.java @@ -76,43 +76,71 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this variant type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare VariantValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof VariantValue)) { throw new IllegalArgumentException("Cannot compare VariantValue with " + other.getClass().getSimpleName()); } - + VariantValue otherVariant = (VariantValue) other; - + // Compare type indices first int indexComparison = Integer.compare(typeIndex, otherVariant.typeIndex); if (indexComparison != 0) { return indexComparison; } - + // If type indices are the same, compare the items return compareValues(item, otherVariant.item); } - + private static int compareValues(Value a, Value b) { // Handle null values - if (a == null && b == null) return 0; - if (a == null) return -1; - if (b == null) return 1; - + if (a == null && b == null) { + return 0; + } + if (a == null) { + return -1; + } + if (b == null) { + return 1; + } + // Check that the types are the same if (!a.getType().equals(b.getType())) { - throw new IllegalArgumentException("Cannot compare values of different types: " + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getType() + " vs " + b.getType()); } - + // Use the actual compareTo method of the values if (a instanceof Comparable && b instanceof Comparable) { try { return ((Comparable>) a).compareTo((Value) b); - } catch (ClassCastException e) {} + } catch (ClassCastException e) { + // Fall back to error + } } - - throw new IllegalArgumentException("Cannot compare values of different types: " + + + throw new IllegalArgumentException("Cannot compare values of different types: " + a.getClass().getSimpleName() + " vs " + b.getClass().getSimpleName()); } } diff --git a/table/src/main/java/tech/ydb/table/values/VoidValue.java b/table/src/main/java/tech/ydb/table/values/VoidValue.java index 0e223953..7997604e 100644 --- a/table/src/main/java/tech/ydb/table/values/VoidValue.java +++ b/table/src/main/java/tech/ydb/table/values/VoidValue.java @@ -48,11 +48,31 @@ public int compareTo(Value other) { if (other == null) { throw new IllegalArgumentException("Cannot compare with null value"); } - + + // Handle comparison with OptionalValue + if (other instanceof OptionalValue) { + OptionalValue otherOptional = (OptionalValue) other; + + // Check that the item type matches this void type + if (!getType().equals(otherOptional.getType().getItemType())) { + throw new IllegalArgumentException( + "Cannot compare VoidValue with OptionalValue of different item type: " + + getType() + " vs " + otherOptional.getType().getItemType()); + } + + // Non-empty value is greater than empty optional + if (!otherOptional.isPresent()) { + return 1; + } + + // Compare with the wrapped value + return compareTo(otherOptional.get()); + } + if (!(other instanceof VoidValue)) { throw new IllegalArgumentException("Cannot compare VoidValue with " + other.getClass().getSimpleName()); } - + // All VoidValue instances are equal return 0; } diff --git a/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java index 9ff82e04..c5e0ad98 100644 --- a/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java +++ b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java @@ -3,9 +3,6 @@ import org.junit.Test; import static org.junit.Assert.*; -import java.util.Arrays; -import java.util.List; - /** * Test for Comparable implementation of Value classes */ @@ -129,7 +126,7 @@ public void testOptionalValueComparison() { OptionalValue opt1 = OptionalValue.of(PrimitiveValue.newInt32(1)); OptionalValue opt2 = OptionalValue.of(PrimitiveValue.newInt32(2)); OptionalValue opt3 = OptionalValue.of(PrimitiveValue.newInt32(1)); - OptionalValue opt4 = OptionalValue.of(null); + OptionalValue opt4 = PrimitiveType.Int32.makeOptional().emptyValue(); assertTrue(opt1.compareTo(opt2) < 0); assertTrue(opt2.compareTo(opt1) > 0); @@ -267,29 +264,4 @@ public void testCompareDifferentTypes() { PrimitiveValue textValue = PrimitiveValue.newText("abc"); intValue.compareTo(textValue); } - - @Test - public void testSorting() { - List> values = Arrays.asList( - PrimitiveValue.newInt32(3), - PrimitiveValue.newInt32(1), - PrimitiveValue.newInt32(2), - PrimitiveValue.newText("abc"), - PrimitiveValue.newText("aaa") - ); - - values.sort((a, b) -> { - // Compare by type kind first, then by value - int typeComparison = a.getType().getKind().compareTo(b.getType().getKind()); - if (typeComparison != 0) { - return typeComparison; - } - return a.toString().compareTo(b.toString()); - }); - - // Verify sorting worked - assertTrue(values.get(0).toString().contains("1")); - assertTrue(values.get(1).toString().contains("2")); - assertTrue(values.get(2).toString().contains("3")); - } } \ No newline at end of file From a299385fb5f29f97d3d900656add6435c65e10c6 Mon Sep 17 00:00:00 2001 From: Maksim Zinal Date: Wed, 30 Jul 2025 23:35:12 +0300 Subject: [PATCH 3/4] fixed PrimitiveValues comparisons --- .../tech/ydb/table/values/PrimitiveValue.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java index 9613f1d7..5e9d84b7 100644 --- a/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java +++ b/table/src/main/java/tech/ydb/table/values/PrimitiveValue.java @@ -499,27 +499,35 @@ public int compareTo(Value other) { case Yson: return compareByteArrays(getBytesUnsafe(), otherPrimitive.getBytesUnsafe()); case Text: + return getText().compareTo(otherPrimitive.getText()); case Json: + return getJson().compareTo(otherPrimitive.getJson()); case JsonDocument: - return getText().compareTo(otherPrimitive.getText()); + return getJsonDocument().compareTo(otherPrimitive.getJsonDocument()); case Uuid: return getUuidJdk().compareTo(otherPrimitive.getUuidJdk()); case Date: - case Date32: return getDate().compareTo(otherPrimitive.getDate()); + case Date32: + return getDate32().compareTo(otherPrimitive.getDate32()); case Datetime: - case Datetime64: return getDatetime().compareTo(otherPrimitive.getDatetime()); + case Datetime64: + return getDatetime64().compareTo(otherPrimitive.getDatetime64()); case Timestamp: - case Timestamp64: return getTimestamp().compareTo(otherPrimitive.getTimestamp()); + case Timestamp64: + return getTimestamp64().compareTo(otherPrimitive.getTimestamp64()); case Interval: - case Interval64: return getInterval().compareTo(otherPrimitive.getInterval()); + case Interval64: + return getInterval64().compareTo(otherPrimitive.getInterval64()); case TzDate: + return getTzDate().compareTo(otherPrimitive.getTzDate()); case TzDatetime: + return getTzDatetime().compareTo(otherPrimitive.getTzDatetime()); case TzTimestamp: - return getTzDate().compareTo(otherPrimitive.getTzDate()); + return getTzTimestamp().compareTo(otherPrimitive.getTzTimestamp()); default: throw new UnsupportedOperationException("Comparison not supported for type: " + getType()); } From 85785c5c04324a9b06399965dd42be271a106792 Mon Sep 17 00:00:00 2001 From: Maksim Zinal Date: Wed, 30 Jul 2025 23:46:28 +0300 Subject: [PATCH 4/4] proper comparison for DictValue --- .../java/tech/ydb/table/values/DictValue.java | 15 +-- .../ydb/table/values/ValueComparableTest.java | 109 +++++++++++++----- 2 files changed, 87 insertions(+), 37 deletions(-) diff --git a/table/src/main/java/tech/ydb/table/values/DictValue.java b/table/src/main/java/tech/ydb/table/values/DictValue.java index 61fc2037..e2069d92 100644 --- a/table/src/main/java/tech/ydb/table/values/DictValue.java +++ b/table/src/main/java/tech/ydb/table/values/DictValue.java @@ -151,12 +151,6 @@ public int compareTo(Value other) { DictValue otherDict = (DictValue) other; - // Compare sizes first - int sizeComparison = Integer.compare(items.size(), otherDict.items.size()); - if (sizeComparison != 0) { - return sizeComparison; - } - // Convert to sorted lists for lexicographical comparison List, Value>> thisEntries = new ArrayList<>(items.entrySet()); List, Value>> otherEntries = new ArrayList<>(otherDict.items.entrySet()); @@ -178,8 +172,9 @@ public int compareTo(Value other) { return compareValues(e1.getValue(), e2.getValue()); }); - // Compare sorted entries - for (int i = 0; i < thisEntries.size(); i++) { + // Compare sorted entries lexicographically + int minLength = Math.min(thisEntries.size(), otherEntries.size()); + for (int i = 0; i < minLength; i++) { Map.Entry, Value> thisEntry = thisEntries.get(i); Map.Entry, Value> otherEntry = otherEntries.get(i); @@ -194,7 +189,9 @@ public int compareTo(Value other) { } } - return 0; + // If we reach here, one dict is a prefix of the other + // The shorter dict comes first + return Integer.compare(thisEntries.size(), otherEntries.size()); } private static int compareValues(Value a, Value b) { diff --git a/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java index c5e0ad98..8fbc9e57 100644 --- a/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java +++ b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java @@ -1,5 +1,7 @@ package tech.ydb.table.values; +import java.util.HashMap; +import java.util.Map; import org.junit.Test; import static org.junit.Assert.*; @@ -14,24 +16,24 @@ public void testPrimitiveValueComparison() { PrimitiveValue int1 = PrimitiveValue.newInt32(1); PrimitiveValue int2 = PrimitiveValue.newInt32(2); PrimitiveValue int3 = PrimitiveValue.newInt32(1); - + assertTrue(int1.compareTo(int2) < 0); assertTrue(int2.compareTo(int1) > 0); assertEquals(0, int1.compareTo(int3)); - + // Test string comparisons PrimitiveValue text1 = PrimitiveValue.newText("abc"); PrimitiveValue text2 = PrimitiveValue.newText("def"); PrimitiveValue text3 = PrimitiveValue.newText("abc"); - + assertTrue(text1.compareTo(text2) < 0); assertTrue(text2.compareTo(text1) > 0); assertEquals(0, text1.compareTo(text3)); - + // Test boolean comparisons PrimitiveValue bool1 = PrimitiveValue.newBool(false); PrimitiveValue bool2 = PrimitiveValue.newBool(true); - + assertTrue(bool1.compareTo(bool2) < 0); assertTrue(bool2.compareTo(bool1) > 0); } @@ -42,7 +44,7 @@ public void testListValueComparison() { ListValue list2 = ListValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(3)); ListValue list3 = ListValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(2)); ListValue list4 = ListValue.of(PrimitiveValue.newInt32(1)); - + assertTrue(list1.compareTo(list2) < 0); assertTrue(list2.compareTo(list1) > 0); assertEquals(0, list1.compareTo(list3)); @@ -54,15 +56,15 @@ public void testListValueLexicographical() { // Test proper lexicographical ordering ListValue list1 = ListValue.of(PrimitiveValue.newText("A"), PrimitiveValue.newText("Z")); ListValue list2 = ListValue.of(PrimitiveValue.newText("Z")); - + // ('Z') should be "bigger" than ('A','Z') in lexicographical order assertTrue(list1.compareTo(list2) < 0); // ('A','Z') < ('Z') assertTrue(list2.compareTo(list1) > 0); // ('Z') > ('A','Z') - + // Test prefix ordering ListValue list3 = ListValue.of(PrimitiveValue.newText("A")); ListValue list4 = ListValue.of(PrimitiveValue.newText("A"), PrimitiveValue.newText("B")); - + assertTrue(list3.compareTo(list4) < 0); // ('A') < ('A','B') assertTrue(list4.compareTo(list3) > 0); // ('A','B') > ('A') } @@ -79,7 +81,7 @@ public void testStructValueComparison() { StructValue struct1 = StructValue.of("a", PrimitiveValue.newInt32(1), "b", PrimitiveValue.newInt32(2)); StructValue struct2 = StructValue.of("a", PrimitiveValue.newInt32(1), "b", PrimitiveValue.newInt32(3)); StructValue struct3 = StructValue.of("a", PrimitiveValue.newInt32(1), "b", PrimitiveValue.newInt32(2)); - + assertTrue(struct1.compareTo(struct2) < 0); assertTrue(struct2.compareTo(struct1) > 0); assertEquals(0, struct1.compareTo(struct3)); @@ -90,7 +92,7 @@ public void testStructValueLexicographical() { // Test proper lexicographical ordering StructValue struct1 = StructValue.of("a", PrimitiveValue.newText("A"), "b", PrimitiveValue.newText("Z")); StructValue struct2 = StructValue.of("a", PrimitiveValue.newText("Z")); - + // ('Z') should be "bigger" than ('A','Z') in lexicographical order assertTrue(struct1.compareTo(struct2) < 0); // ('A','Z') < ('Z') assertTrue(struct2.compareTo(struct1) > 0); // ('Z') > ('A','Z') @@ -108,7 +110,7 @@ public void testDictValueComparison() { DictValue dict1 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); DictValue dict2 = DictValue.of(PrimitiveValue.newText("b"), PrimitiveValue.newInt32(1)); DictValue dict3 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); - + assertTrue(dict1.compareTo(dict2) < 0); assertTrue(dict2.compareTo(dict1) > 0); assertEquals(0, dict1.compareTo(dict3)); @@ -121,13 +123,64 @@ public void testDictValueDifferentTypes() { dict1.compareTo(dict2); // Should throw exception for different value types } + @Test + public void testDictValueLexicographical() { + // Test proper lexicographical ordering - content matters more than size + + // Case 1: Shorter dict with "larger" key should be greater than longer dict with "smaller" keys + Map, Value> map1 = new HashMap<>(); + map1.put(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + map1.put(PrimitiveValue.newText("b"), PrimitiveValue.newInt32(2)); + DictValue dict1 = DictType.of(PrimitiveType.Text, PrimitiveType.Int32).newValueOwn(map1); + + DictValue dict2 = DictValue.of(PrimitiveValue.newText("z"), PrimitiveValue.newInt32(1)); + + // {"z": 1} should be "bigger" than {"a": 1, "b": 2} in lexicographical order + assertTrue(dict1.compareTo(dict2) < 0); // {"a": 1, "b": 2} < {"z": 1} + assertTrue(dict2.compareTo(dict1) > 0); // {"z": 1} > {"a": 1, "b": 2} + + // Case 2: Same keys, different values - value comparison matters + DictValue dict3 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + DictValue dict4 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(2)); + + assertTrue(dict3.compareTo(dict4) < 0); // {"a": 1} < {"a": 2} + assertTrue(dict4.compareTo(dict3) > 0); // {"a": 2} > {"a": 1} + + // Case 3: One dict is a prefix of another - shorter comes first only if it's a prefix + DictValue dict5 = DictValue.of(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + + Map, Value> map6 = new HashMap<>(); + map6.put(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + map6.put(PrimitiveValue.newText("b"), PrimitiveValue.newInt32(2)); + DictValue dict6 = DictType.of(PrimitiveType.Text, PrimitiveType.Int32).newValueOwn(map6); + + assertTrue(dict5.compareTo(dict6) < 0); // {"a": 1} < {"a": 1, "b": 2} (prefix case) + assertTrue(dict6.compareTo(dict5) > 0); // {"a": 1, "b": 2} > {"a": 1} + + // Case 4: Multiple entries with different ordering + Map, Value> map7 = new HashMap<>(); + map7.put(PrimitiveValue.newText("x"), PrimitiveValue.newInt32(1)); + map7.put(PrimitiveValue.newText("y"), PrimitiveValue.newInt32(1)); + DictValue dict7 = DictType.of(PrimitiveType.Text, PrimitiveType.Int32).newValueOwn(map7); + + Map, Value> map8 = new HashMap<>(); + map8.put(PrimitiveValue.newText("a"), PrimitiveValue.newInt32(1)); + map8.put(PrimitiveValue.newText("b"), PrimitiveValue.newInt32(1)); + map8.put(PrimitiveValue.newText("c"), PrimitiveValue.newInt32(1)); + DictValue dict8 = DictType.of(PrimitiveType.Text, PrimitiveType.Int32).newValueOwn(map8); + + // {"x": 1, "y": 1} should be greater than {"a": 1, "b": 1, "c": 1} despite being shorter + assertTrue(dict8.compareTo(dict7) < 0); // {"a": 1, "b": 1, "c": 1} < {"x": 1, "y": 1} + assertTrue(dict7.compareTo(dict8) > 0); // {"x": 1, "y": 1} > {"a": 1, "b": 1, "c": 1} + } + @Test public void testOptionalValueComparison() { OptionalValue opt1 = OptionalValue.of(PrimitiveValue.newInt32(1)); OptionalValue opt2 = OptionalValue.of(PrimitiveValue.newInt32(2)); OptionalValue opt3 = OptionalValue.of(PrimitiveValue.newInt32(1)); OptionalValue opt4 = PrimitiveType.Int32.makeOptional().emptyValue(); - + assertTrue(opt1.compareTo(opt2) < 0); assertTrue(opt2.compareTo(opt1) > 0); assertEquals(0, opt1.compareTo(opt3)); @@ -148,15 +201,15 @@ public void testOptionalValueWithNonOptional() { OptionalValue opt3 = PrimitiveType.Int32.makeOptional().emptyValue(); PrimitiveValue prim1 = PrimitiveValue.newInt32(1); PrimitiveValue prim2 = PrimitiveValue.newInt32(2); - + // Optional with non-optional of same type assertEquals(0, opt1.compareTo(prim1)); // Same value assertTrue(opt1.compareTo(prim2) < 0); // Optional value less than non-optional assertTrue(opt2.compareTo(prim1) > 0); // Optional value greater than non-optional - + // Empty optional with non-optional assertTrue(opt3.compareTo(prim1) < 0); // Empty < non-empty - + // Non-optional with optional assertEquals(0, prim1.compareTo(opt1)); // Same value assertTrue(prim1.compareTo(opt2) < 0); // Non-optional less than optional @@ -177,7 +230,7 @@ public void testTupleValueComparison() { TupleValue tuple2 = TupleValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(3)); TupleValue tuple3 = TupleValue.of(PrimitiveValue.newInt32(1), PrimitiveValue.newInt32(2)); TupleValue tuple4 = TupleValue.of(PrimitiveValue.newInt32(1)); - + assertTrue(tuple1.compareTo(tuple2) < 0); assertTrue(tuple2.compareTo(tuple1) > 0); assertEquals(0, tuple1.compareTo(tuple3)); @@ -189,7 +242,7 @@ public void testTupleValueLexicographical() { // Test proper lexicographical ordering TupleValue tuple1 = TupleValue.of(PrimitiveValue.newText("A"), PrimitiveValue.newText("Z")); TupleValue tuple2 = TupleValue.of(PrimitiveValue.newText("Z")); - + // ('Z') should be "bigger" than ('A','Z') in lexicographical order assertTrue(tuple1.compareTo(tuple2) < 0); // ('A','Z') < ('Z') assertTrue(tuple2.compareTo(tuple1) > 0); // ('Z') > ('A','Z') @@ -204,13 +257,13 @@ public void testTupleValueDifferentTypes() { @Test public void testVariantValueComparison() { - VariantValue variant1 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + VariantValue variant1 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), PrimitiveValue.newInt32(1), 0); - VariantValue variant2 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + VariantValue variant2 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), PrimitiveValue.newText("abc"), 1); - VariantValue variant3 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + VariantValue variant3 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), PrimitiveValue.newInt32(2), 0); - + assertTrue(variant1.compareTo(variant2) < 0); // type index 0 < 1 assertTrue(variant2.compareTo(variant1) > 0); assertTrue(variant1.compareTo(variant3) < 0); // same type index, compare values @@ -218,9 +271,9 @@ public void testVariantValueComparison() { @Test(expected = IllegalArgumentException.class) public void testVariantValueDifferentTypes() { - VariantValue variant1 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + VariantValue variant1 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), PrimitiveValue.newInt32(1), 0); - VariantValue variant2 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), + VariantValue variant2 = new VariantValue(VariantType.ofOwn(PrimitiveType.Int32, PrimitiveType.Text), PrimitiveValue.newText("abc"), 0); variant1.compareTo(variant2); // Should throw exception for different item types } @@ -229,7 +282,7 @@ public void testVariantValueDifferentTypes() { public void testVoidValueComparison() { VoidValue void1 = VoidValue.of(); VoidValue void2 = VoidValue.of(); - + assertEquals(0, void1.compareTo(void2)); } @@ -237,7 +290,7 @@ public void testVoidValueComparison() { public void testNullValueComparison() { NullValue null1 = NullValue.of(); NullValue null2 = NullValue.of(); - + assertEquals(0, null1.compareTo(null2)); } @@ -246,7 +299,7 @@ public void testDecimalValueComparison() { DecimalValue decimal1 = DecimalValue.fromLong(DecimalType.of(10, 2), 100); DecimalValue decimal2 = DecimalValue.fromLong(DecimalType.of(10, 2), 200); DecimalValue decimal3 = DecimalValue.fromLong(DecimalType.of(10, 2), 100); - + assertTrue(decimal1.compareTo(decimal2) < 0); assertTrue(decimal2.compareTo(decimal1) > 0); assertEquals(0, decimal1.compareTo(decimal3)); @@ -264,4 +317,4 @@ public void testCompareDifferentTypes() { PrimitiveValue textValue = PrimitiveValue.newText("abc"); intValue.compareTo(textValue); } -} \ No newline at end of file +} \ No newline at end of file