Skip to content

Commit b03b8b5

Browse files
committed
#135: Allow queries with no particular sort order
1 parent 6cf9fa8 commit b03b8b5

File tree

7 files changed

+125
-6
lines changed

7 files changed

+125
-6
lines changed

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.google.common.base.Preconditions;
44
import lombok.NonNull;
55
import lombok.Value;
6+
import tech.ydb.yoj.ExperimentalApi;
67
import tech.ydb.yoj.databind.schema.Schema;
78

89
import java.util.List;
@@ -26,11 +27,58 @@ public <U> OrderExpression<U> forSchema(@NonNull Schema<U> dstSchema,
2627
.collect(toList()));
2728
}
2829

30+
/**
31+
* Returns an {@code OrderExpression} that applies <em>no particular sort order</em> for the query results.
32+
* The results will be returned in an implementation-defined order which is subject to change at any time,
33+
* <em>potentially even giving a different ordering for repeated executions of the same query</em>.
34+
* <p>This is different from the {@code OrderExpression} being {@code null} or not specified,
35+
* which YOJ interprets as "order query results by entity ID ascending" to ensure maximum
36+
* predictability of query results.
37+
* <p><strong>BEWARE!</strong> For small queries that return results entirely from a single YDB table partition
38+
* (<em>data shard</em>), the <em>no particular sort order</em> imposed by {@code OrderExpression.unordered()}
39+
* on a real YDB database will <strong>most likely be the same</strong> as "order by entity ID ascending",
40+
* but this will quickly and unpredictably change if the table and/or the result set grow bigger.
41+
*
42+
* @param schema schema to use
43+
* @return an {@code OrderExpression} representing <em>no particular sort order</em>
44+
*
45+
* @param <U> schema type
46+
*/
47+
@NonNull
48+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115")
49+
public static <U> OrderExpression<U> unordered(@NonNull Schema<U> schema) {
50+
return new OrderExpression<>(schema);
51+
}
52+
53+
/**
54+
* @return A fresh {@code Stream} of {@link SortKey sort keys} that this expression has.
55+
* Will be empty if this expression represents an {@link #isUnordered() arbitrary sort order}.
56+
*/
2957
@NonNull
3058
public Stream<SortKey> keyStream() {
3159
return keys.stream();
3260
}
3361

62+
/**
63+
* @return {@code true} if this {@code OrderExpression} represents a well-defined sort order;
64+
* {@code false} if the sort order is arbitrary
65+
*
66+
* @see #isUnordered()
67+
*/
68+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115")
69+
public boolean isOrdered() {
70+
return !keys.isEmpty();
71+
}
72+
73+
/**
74+
* @return {@code true} if this {@code OrderExpression} represents arbitrary sort order (whatever the database returns);
75+
* {@code false} otherwise
76+
*/
77+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115")
78+
public boolean isUnordered() {
79+
return !isOrdered();
80+
}
81+
3482
@Override
3583
public String toString() {
3684
return keys.stream().map(Object::toString).collect(joining(", "));
@@ -42,6 +90,11 @@ public OrderExpression(@NonNull Schema<T> schema, @NonNull List<SortKey> keys) {
4290
this.keys = keys;
4391
}
4492

93+
private OrderExpression(@NonNull Schema<T> schema) {
94+
this.schema = schema;
95+
this.keys = List.of();
96+
}
97+
4598
@Value
4699
public static class SortKey {
47100
Schema.JavaField field;

repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlListingQuery.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ private static void escapeLikePatternToSb(@NonNull String str, StringBuilder sb)
165165
}
166166

167167
public static <T extends Entity<T>> YqlOrderBy toYqlOrderBy(@NonNull OrderExpression<T> orderBy) {
168-
return YqlOrderBy.orderBy(orderBy.getKeys().stream().map(YqlListingQuery::toSortKey).collect(toList()));
168+
return orderBy.isUnordered()
169+
? YqlOrderBy.unordered()
170+
: YqlOrderBy.orderBy(orderBy.getKeys().stream().map(YqlListingQuery::toSortKey).collect(toList()));
169171
}
170172

171173
private static YqlOrderBy.SortKey toSortKey(SortKey k) {

repository-ydb-v2/src/main/java/tech/ydb/yoj/repository/ydb/yql/YqlOrderBy.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import lombok.Getter;
77
import lombok.NonNull;
88
import lombok.Value;
9+
import tech.ydb.yoj.ExperimentalApi;
910
import tech.ydb.yoj.repository.db.Entity;
1011
import tech.ydb.yoj.repository.db.EntitySchema;
1112

@@ -25,6 +26,8 @@
2526
@Getter
2627
@EqualsAndHashCode
2728
public final class YqlOrderBy implements YqlStatementPart<YqlOrderBy> {
29+
private static final YqlOrderBy UNORDERED = new YqlOrderBy(List.of());
30+
2831
public static final String TYPE = "OrderBy";
2932
private final List<SortKey> keys;
3033

@@ -95,6 +98,22 @@ public static YqlOrderBy orderBy(Collection<SortKey> keys) {
9598
return new Builder().by(keys).build();
9699
}
97100

101+
/**
102+
* Returns an {@code YqlOrderBy} that applies <em>no particular sort order</em> to the query results.
103+
* The results will be returned in an implementation-defined order which is subject to change at any time,
104+
* <em>potentially even giving a different ordering for repeated executions of the same query</em>.
105+
* <p><strong>BEWARE!</strong> For small queries that return results entirely from a single table partition
106+
* (<em>data shard</em>), the <em>no particular sort order</em> imposed by {@code YqlOrderBy.unordered()}
107+
* on a real YDB database will <strong>most likely be the same</strong> as "order by entity ID ascending",
108+
* but this will quickly and unpredictably change if the table and/or the result set grow bigger.
109+
*
110+
* @return an {@code YqlOrderBy} representing <em>no particular sort order</em>
111+
*/
112+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115")
113+
public static YqlOrderBy unordered() {
114+
return UNORDERED;
115+
}
116+
98117
/**
99118
* @return builder for {@code YqlOrderBy}
100119
*/
@@ -104,7 +123,7 @@ public static Builder order() {
104123

105124
@Override
106125
public <T extends Entity<T>> String toYql(@NonNull EntitySchema<T> schema) {
107-
return keys.stream().map(k -> k.toYql(schema)).collect(joining(", "));
126+
return keys.isEmpty() ? "" : keys.stream().map(k -> k.toYql(schema)).collect(joining(", "));
108127
}
109128

110129
@Override
@@ -114,7 +133,7 @@ public String getType() {
114133

115134
@Override
116135
public String getYqlPrefix() {
117-
return "ORDER BY ";
136+
return keys.isEmpty() ? "" : "ORDER BY ";
118137
}
119138

120139
@Override
@@ -124,7 +143,7 @@ public int getPriority() {
124143

125144
@Override
126145
public String toString() {
127-
return format("order by %s", keys.stream().map(Object::toString).collect(joining(", ")));
146+
return keys.isEmpty() ? "unordered" : format("order by %s", keys.stream().map(Object::toString).collect(joining(", ")));
128147
}
129148

130149
public enum SortOrder {
@@ -147,7 +166,7 @@ public String toString() {
147166
* Sort key: entity field plus {@link SortOrder sort order} (either ascending or descending).
148167
*/
149168
@Value
150-
public static final class SortKey {
169+
public static class SortKey {
151170
String fieldPath;
152171
SortOrder order;
153172

repository-ydb-v2/src/test/java/tech/ydb/yoj/repository/ydb/YdbRepositoryIntegrationTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,22 @@ public void ydbTransactionCompatibility() {
931931
}
932932
}
933933

934+
@Test
935+
public void unordered() {
936+
// YDB tends to return data in index-order, not "by PK ascending" order, if we don't force the result order
937+
IndexedEntity ie1 = new IndexedEntity(new IndexedEntity.Id("abc"), "z", "v1-1", "v1-2");
938+
IndexedEntity ie2 = new IndexedEntity(new IndexedEntity.Id("def"), "y", "v2-1", "v2-2");
939+
db.tx(() -> db.indexedTable().insert(ie1, ie2));
940+
941+
var results = db.tx(() -> db.indexedTable().query()
942+
.where("keyId").gte("a")
943+
.limit(2)
944+
.index(IndexedEntity.KEY_INDEX)
945+
.unordered()
946+
.find());
947+
assertThat(results).containsExactly(ie2, ie1);
948+
}
949+
934950
@AllArgsConstructor
935951
private static class DelegateSchemeServiceImplBase extends SchemeServiceGrpc.SchemeServiceImplBase {
936952
@Delegate

repository/src/main/java/tech/ydb/yoj/repository/db/EntityExpressions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package tech.ydb.yoj.repository.db;
22

33
import lombok.NonNull;
4+
import tech.ydb.yoj.ExperimentalApi;
45
import tech.ydb.yoj.databind.expression.FilterBuilder;
56
import tech.ydb.yoj.databind.expression.OrderBuilder;
67
import tech.ydb.yoj.databind.expression.OrderExpression;
8+
import tech.ydb.yoj.databind.schema.Schema;
79

810
import static tech.ydb.yoj.databind.expression.OrderExpression.SortOrder.ASCENDING;
911

@@ -19,6 +21,14 @@ public static <T extends Entity<T>> OrderBuilder<T> newOrderBuilder(@NonNull Cla
1921
return OrderBuilder.forSchema(schema(entityType));
2022
}
2123

24+
/**
25+
* @see OrderExpression#unordered(Schema)
26+
*/
27+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115")
28+
public static <T extends Entity<T>> OrderExpression<T> unordered(@NonNull Class<T> entityType) {
29+
return OrderExpression.unordered(schema(entityType));
30+
}
31+
2232
private static <T extends Entity<T>> EntitySchema<T> schema(@NonNull Class<T> entityType) {
2333
return EntitySchema.of(entityType);
2434
}

repository/src/main/java/tech/ydb/yoj/repository/db/TableQueryBuilder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import com.google.common.base.Preconditions;
44
import lombok.NonNull;
55
import lombok.RequiredArgsConstructor;
6+
import tech.ydb.yoj.ExperimentalApi;
67
import tech.ydb.yoj.databind.expression.FilterBuilder;
78
import tech.ydb.yoj.databind.expression.FilterExpression;
89
import tech.ydb.yoj.databind.expression.OrderBuilder;
910
import tech.ydb.yoj.databind.expression.OrderExpression;
11+
import tech.ydb.yoj.databind.schema.Schema;
1012

1113
import javax.annotation.Nullable;
1214
import java.util.Collection;
@@ -197,6 +199,15 @@ private FilterExpression<T> getFinalFilter() {
197199
}
198200
}
199201

202+
/**
203+
* @see OrderExpression#unordered(Schema)
204+
*/
205+
@NonNull
206+
@ExperimentalApi(issue = "https://github.com/ydb-platform/yoj-project/issues/115")
207+
public TableQueryBuilder<T> unordered() {
208+
return orderBy(EntityExpressions.unordered(table.getType()));
209+
}
210+
200211
@NonNull
201212
public TableQueryBuilder<T> orderBy(@Nullable OrderExpression<T> orderBy) {
202213
this.orderBy = orderBy;

repository/src/main/java/tech/ydb/yoj/repository/db/list/InMemoryQueries.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
import javax.annotation.Nullable;
1919
import java.util.Comparator;
20+
import java.util.IdentityHashMap;
2021
import java.util.List;
2122
import java.util.Map;
23+
import java.util.UUID;
2224
import java.util.function.Function;
2325
import java.util.function.Predicate;
2426
import java.util.stream.Stream;
@@ -144,12 +146,18 @@ public Predicate<T> visitNotExpr(@NonNull NotExpr<T> notExpr) {
144146
}
145147

146148
public static <T extends Entity<T>> Comparator<T> toComparator(@NonNull OrderExpression<T> orderBy) {
149+
if (orderBy.isUnordered()) {
150+
// Produces a randomly-ordering, but stable Comparator. UUID.randomUUID() collisions are extremely unlikely, so we ignore them.
151+
Map<Object, UUID> randomIds = new IdentityHashMap<>();
152+
return Comparator.comparing(e -> randomIds.computeIfAbsent(e, __ -> UUID.randomUUID()));
153+
}
154+
147155
Schema<T> schema = orderBy.getSchema();
148156
return (a, b) -> {
149157
Map<String, Object> mapA = schema.flatten(a);
150158
Map<String, Object> mapB = schema.flatten(b);
151159
for (OrderExpression.SortKey sortKey : orderBy.getKeys()) {
152-
for (JavaField field : sortKey.getField().flatten().collect(toList())) {
160+
for (JavaField field : sortKey.getField().flatten().toList()) {
153161
int res = compare(FieldValue.getComparable(mapA, field), FieldValue.getComparable(mapB, field));
154162
if (res != 0) {
155163
return sortKey.getOrder() == ASCENDING ? res : -res;

0 commit comments

Comments
 (0)