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..41464acf 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,85 @@ 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"); + } + + // 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 + 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..e2069d92 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,110 @@ public ValueProtos.Value toPb() { } return builder.build(); } + + @Override + 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; + + // 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 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); + + int keyComparison = compareValues(thisEntry.getKey(), otherEntry.getKey()); + if (keyComparison != 0) { + return keyComparison; + } + + int valueComparison = compareValues(thisEntry.getValue(), otherEntry.getValue()); + if (valueComparison != 0) { + return valueComparison; + } + } + + // 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) { + // 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) { + // Fall back to error + } + } + + 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..f34f9419 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,84 @@ public ValueProtos.Value toPb() { } return builder.build(); } + + @Override + 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; + } + + // 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) { + // Fall back to error + } + } + + 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..b106688b 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,38 @@ 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"); + } + + // 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 71c8d3f1..dde545c6 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,65 @@ 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) { + // Fall back to error + } + } + 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..5e9d84b7 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,116 @@ 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"); + } + + // 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()); + } + + 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: + return getText().compareTo(otherPrimitive.getText()); + case Json: + return getJson().compareTo(otherPrimitive.getJson()); + case JsonDocument: + return getJsonDocument().compareTo(otherPrimitive.getJsonDocument()); + case Uuid: + return getUuidJdk().compareTo(otherPrimitive.getUuidJdk()); + case Date: + return getDate().compareTo(otherPrimitive.getDate()); + case Date32: + return getDate32().compareTo(otherPrimitive.getDate32()); + case Datetime: + return getDatetime().compareTo(otherPrimitive.getDatetime()); + case Datetime64: + return getDatetime64().compareTo(otherPrimitive.getDatetime64()); + case Timestamp: + return getTimestamp().compareTo(otherPrimitive.getTimestamp()); + case Timestamp64: + return getTimestamp64().compareTo(otherPrimitive.getTimestamp64()); + case Interval: + 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 getTzTimestamp().compareTo(otherPrimitive.getTzTimestamp()); + 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..196771c2 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,86 @@ public ValueProtos.Value toPb() { return builder.build(); } + @Override + 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; + } + + // 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) { + // Fall back to error + } + } + + 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..00f16ed4 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,84 @@ 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"); + } + + // 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; + } + + // 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) { + // Fall back to error + } + } + + 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..d2186708 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..dda7842e 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,77 @@ 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"); + } + + // 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; + } + + // 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) { + // Fall back to error + } + } + + 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..7997604e 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,38 @@ 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"); + } + + // 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 new file mode 100644 index 00000000..8fbc9e57 --- /dev/null +++ b/table/src/test/java/tech/ydb/table/values/ValueComparableTest.java @@ -0,0 +1,320 @@ +package tech.ydb.table.values; + +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * 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 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)); + 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); + } +} \ No newline at end of file