From f8d3915cf034a79607bc6095febc0e561b9e8f13 Mon Sep 17 00:00:00 2001 From: Youssef Hatem Date: Wed, 11 Nov 2026 10:07:03 +0000 Subject: [PATCH] Compare `ByteString` keys in `KeyComparisons` A Cascades union sorted through an order function such as `order_desc_nulls_last` compares by `to_ordered_bytes(...)`, which evaluates to a `ByteString`. `FIELD_COMPARATOR` handled `byte[]` but not `ByteString`, so the comparison fell through to `Comparable` and threw a `ClassCastException` as soon as two legs tied on the preceding key columns. Compare them unsigned lexicographically, which is the order the ordered-bytes encoding is built for. --- .../foundationdb/cursors/KeyComparisons.java | 3 +++ .../query/FDBOrderingQueryTest.java | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/cursors/KeyComparisons.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/cursors/KeyComparisons.java index 2ef91a7219b..9f344c04458 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/cursors/KeyComparisons.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/cursors/KeyComparisons.java @@ -22,6 +22,7 @@ import com.apple.foundationdb.annotation.API; import com.apple.foundationdb.tuple.ByteArrayUtil; +import com.google.protobuf.ByteString; import java.util.Comparator; import java.util.List; @@ -43,6 +44,8 @@ public class KeyComparisons { return 1; } else if (o1 instanceof byte[]) { return ByteArrayUtil.compareUnsigned((byte[])o1, (byte[])o2); + } else if (o1 instanceof ByteString) { + return ByteString.unsignedLexicographicalComparator().compare((ByteString)o1, (ByteString)o2); } else if (o1 instanceof List) { return KeyComparisons.KEY_COMPARATOR.compare((List)o1, (List)o2); } else { diff --git a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/query/FDBOrderingQueryTest.java b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/query/FDBOrderingQueryTest.java index 8d64222b0c3..103ac9318ae 100644 --- a/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/query/FDBOrderingQueryTest.java +++ b/fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/foundationdb/query/FDBOrderingQueryTest.java @@ -331,6 +331,32 @@ void testUnionOrdered3() throws Exception { } } + /** + * Union legs that tie on {@code num_value_2} have to be compared on the ordered bytes of {@code str_value_indexed}. + */ + @DualPlannerTest + void testUnionOrderedOverlappingRanges() throws Exception { + final RecordMetaDataHook hook = indexHook("order_desc_nulls_last"); + loadRecords(hook); + final RecordQuery query = RecordQuery.newBuilder() + .setRecordType("MySimpleRecord") + .setSort(fullOrderingKey("order_desc_nulls_last")) + .setFilter(Query.or( + Query.and(Query.field("num_value_2").equalsValue(101), + Query.field("str_value_indexed").greaterThanOrEquals("a"), Query.field("str_value_indexed").lessThan("b")), + Query.and(Query.field("num_value_2").equalsValue(101), + Query.field("str_value_indexed").greaterThan("a"), Query.field("str_value_indexed").lessThanOrEquals("b")))) + .build(); + try (FDBRecordContext context = openContext()) { + openSimpleRecordStore(context, hook); + final RecordQueryPlan plan = planner.planQuery(query).getPlan(); + final List actual = plan.execute(recordStore) + .map(r -> r.getPrimaryKey().getLong(0)) + .asList().join(); + assertEquals(List.of(5L, 4L, 3L), actual); + } + } + @DualPlannerTest void testCoveringOrdered() throws Exception { final RecordMetaDataHook hook = indexHook("order_desc_nulls_last");