Skip to content

Commit

Permalink
noticket: Minor YqlStatementPart cleanup
Browse files Browse the repository at this point in the history
- Stricter default implementation of `YqlStatementPart.combine()` that throws `UnsupportedOperationException`
- Removed the (incorrect and strange) `YqlOrderBy.combine()` implementation that ignores `this.sortKeys`
*and* does not attempt to de-duplicate sort keys, allowing multiple occurrences of the same sort key, even
with a different sort direction.
Known YOJ users are unlikely to have used the `YqlOrderBy.combine()` functionality, ever.
- Better function signature for `YqlStatement.mergeParts()` that combines `YqlStatementPart`s of the same type.
It no longer takes a `Stream` of `YqlStatementPart`s but a `Collection` of them, which is appropriate for
most usages. Implementations of custom statements (especially inheriting from `PredicateStatement`) will need
to eventually transition to the new `mergeParts` variant.
- Improved consistency in `YqlLimit` and `YqlView`: use `getXyz()` POJO-style getters instead of `xyz()`,
deprecate the confusingly named `YqlLimit.size()` method in favor of explicit `getLimit()` and `getOffset()`
getters which are more general.
  • Loading branch information
nvamelichev committed Jan 28, 2025
1 parent e6c11f7 commit 3e2a5d7
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public String getQuery(String tablespace) {
return declarations()
+ "SELECT COUNT(*) AS count"
+ " FROM " + table(tablespace)
+ " " + mergeParts(parts.stream())
+ " " + mergeParts(parts)
.sorted(comparing(YqlStatementPart::getPriority))
.map(sp -> sp.toFullYql(schema))
.map(this::resolveParamNames)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public String getQuery(String tablespace) {
return declarations()
+ "SELECT " + (distinct ? "DISTINCT " : "") + outNames()
+ " FROM " + table(tablespace)
+ " " + mergeParts(parts.stream())
+ " " + mergeParts(parts)
.sorted(comparing(YqlStatementPart::getPriority))
.map(sp -> sp.toFullYql(schema))
.map(this::resolveParamNames)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.google.protobuf.NullValue;
import lombok.Getter;
import tech.ydb.proto.ValueProtos;
import tech.ydb.yoj.DeprecationWarnings;
import tech.ydb.yoj.databind.schema.Schema;
import tech.ydb.yoj.repository.db.Entity;
import tech.ydb.yoj.repository.db.EntityIdSchema;
Expand Down Expand Up @@ -78,13 +79,44 @@ public String getDeclaration(String name, String type) {
return String.format("DECLARE %s AS %s;\n", name, type);
}

/**
* Tries to combine/simplify the specified {@link YqlStatementPart statement part}s into a potentially smaller number of statement parts, e.g.,
* joining multiple {@code YqlPredicate}s into a single {@code AND} clause ({@code AndPredicate}).
* <br>Note that this method does not attempt to sort statement parts by {@link YqlStatementPart#getPriority() priority} or perform any
* YQL code generation at all.
* <p><strong>Warning:</strong> A closed/consumed or a partially consumed {@code Stream} could have <em>potentially</em> been passed
* to this method. But in all cases that we know of (both standard and custom {@code YqlStatement}s), this method was always fed a fresh stream,
* obtained by calling {@code someCollection.stream()}. This method is now replaced by the less error-prone {@link #mergeParts(Collection)}.
*
* @deprecated This method is deprecated and will be removed in YOJ 3.0.0. Please use {@link #mergeParts(Collection)} instead.
*/
@Deprecated(forRemoval = true)
@SuppressWarnings("DataFlowIssue")
protected static Stream<? extends YqlStatementPart<?>> mergeParts(Stream<? extends YqlStatementPart<?>> origParts) {
DeprecationWarnings.warnOnce("YqlStatement.mergeParts(Stream)",
"YqlStatement.mergeParts(Stream) is deprecated and will be removed in YOJ 3.0.0. Use YqlStatement.mergeParts(Collection) instead");
return origParts
.collect(groupingBy(YqlStatementPart::getType))
.values().stream()
.flatMap(items -> combine(items).stream());
}

/**
* Tries to combine/simplify the specified {@link YqlStatementPart statement part}s into a potentially smaller number of statement parts, e.g.,
* joining multiple {@code YqlPredicate}s into a single {@code AND} clause ({@code AndPredicate}).
* <br>Note that this method does not attempt to sort statement parts by {@link YqlStatementPart#getPriority() priority} or perform any
* YQL code generation at all.
*
* @param origParts original collection of {@link YqlStatementPart statement parts}
* @return a fresh stream containing potentially combined {@link YqlStatementPart statement parts}
*/
protected static Stream<? extends YqlStatementPart<?>> mergeParts(Collection<? extends YqlStatementPart<?>> origParts) {
return origParts.stream()
.collect(groupingBy(YqlStatementPart::getType))
.values().stream()
.flatMap(items -> combine(items).stream());
}

private static List<? extends YqlStatementPart<?>> combine(List<? extends YqlStatementPart<?>> items) {
if (items.size() < 2) {
return items;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package tech.ydb.yoj.repository.ydb.yql;

import com.google.common.base.Preconditions;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.Value;
import lombok.With;
import lombok.experimental.FieldDefaults;
import tech.ydb.yoj.DeprecationWarnings;
import tech.ydb.yoj.repository.db.Entity;
import tech.ydb.yoj.repository.db.EntitySchema;

import java.util.List;

import static lombok.AccessLevel.PRIVATE;

/**
* Represents a {@code LIMIT ... [OFFSET ...]} clause in a YQL statement.
* <br>Note that YDB does <strong>not</strong> support {@code OFFSET} without {@code LIMIT}, so "row offset" cannot be set directly.
* To return the maximum possible amount of rows, you must know the YDB ResultSet row limit (e.g., defined by {@code YDB_KQP_RESULT_ROWS_LIMIT}
* environment variable for local YDB-in-Docker), and use {@link YqlLimit#range(long, long) YqlLimit.range(offset, max rows + offset)}.
* If you have a specific limit {@code < max rows}, it's much better to use {@link YqlLimit#range(long, long) YqlLimit.range(offset, limit + offset)},
* of course.
*
* @see #top(long)
* @see #range(long, long)
* @see #toYql(EntitySchema)
*/
@EqualsAndHashCode
@FieldDefaults(makeFinal = true, level = PRIVATE)
public final class YqlLimit implements YqlStatementPart<YqlLimit> {
@Value
public class YqlLimit implements YqlStatementPart<YqlLimit> {
/**
* Gives a {@code LIMIT} clause that will always return an empty range ({@code LIMIT 0}).
*/
public static final YqlLimit EMPTY = new YqlLimit(0, 0);

@With
Expand All @@ -40,17 +43,15 @@ private YqlLimit(long limit, long offset) {
* Creates a limit clause to fetch rows in the half-open range {@code [from, to)}.
*
* @param from first row index, counting from 0, inclusive
* @param to last row index, counting from 0, exclusive
* @return limit clause to fetch rows in range {@code [from, to)}
* @param to last row index, counting from 0, exclusive. Must be {@code >= from}
* @return limit clause to fetch {@code (to - from)} rows in range {@code [from, to)}
*/
public static YqlLimit range(long from, long to) {
Preconditions.checkArgument(from >= 0, "from must be >= 0");
Preconditions.checkArgument(to >= 0, "to must be >= 0");
Preconditions.checkArgument(to >= from, "to must be >= from");

long limit = to - from;
long offset = limit == 0 ? 0 : from;
return limit == 0 ? EMPTY : new YqlLimit(limit, offset);
return to == from ? EMPTY : new YqlLimit(to - from, from);
}

/**
Expand All @@ -73,11 +74,22 @@ public static YqlLimit empty() {
return EMPTY;
}

/**
* @deprecated Please calculate the maximum number of rows fetched by this {@code LIMIT} clause by using {@code YqlLimit.getLimit()} and
* {@code YqlLimit.getOffset()} instead.
*/
@Deprecated(forRemoval = true)
public long size() {
DeprecationWarnings.warnOnce("YqlLimit.size()",
"Please calculate range size using YqlLimit.getLimit() and YqlLimit.getOffset() instead of calling YqlLimit.size()");
return limit;
}

/**
* @return {@code true} if this {@code YqlLimit} represents an empty range ({@code LIMIT 0}); {@code false} otherwise
*/
public boolean isEmpty() {
// Does not need to check the offset because with limit == 0, the offset is irrelevant, the DB will return no rows anyway!
return limit == 0;
}

Expand All @@ -98,12 +110,7 @@ public String getYqlPrefix() {

@Override
public <T extends Entity<T>> String toYql(@NonNull EntitySchema<T> schema) {
return size() + (offset == 0 ? "" : " OFFSET " + offset);
}

@Override
public List<? extends YqlStatementPart<?>> combine(@NonNull List<? extends YqlLimit> other) {
throw new UnsupportedOperationException("Multiple LIMIT specifications are not supported");
return limit + (offset == 0 ? "" : " OFFSET " + offset);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,6 @@ public int getPriority() {
return 100;
}

@Override
public List<? extends YqlOrderBy> combine(@NonNull List<? extends YqlOrderBy> other) {
return singletonList(new YqlOrderBy(other.stream()
.flatMap(o -> o.getKeys().stream())
.collect(toList())));
}

@Override
public String toString() {
return format("order by %s", keys.stream().map(Object::toString).collect(joining(", ")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public final List<? extends YqlStatementPart<?>> combine(@NonNull List<? extends
}

/**
* @return stream of statement parameter specifications, if this YQL predicate uses parameters
* @return a fresh stream of statement parameter specifications, if this YQL predicate uses parameters; an fresh empty stream otherwise
*/
public Stream<YqlPredicateParam<?>> paramStream() {
return Stream.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ default <T extends Entity<T>> String toFullYql(@NonNull EntitySchema<T> schema)
<T extends Entity<T>> String toYql(@NonNull EntitySchema<T> schema);

default List<? extends YqlStatementPart<?>> combine(@NonNull List<? extends P> other) {
return other;
throw new UnsupportedOperationException("Multiple " + getType() + " specifications are not supported");
}
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
package tech.ydb.yoj.repository.ydb.yql;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.With;
import lombok.experimental.FieldDefaults;
import tech.ydb.yoj.DeprecationWarnings;
import tech.ydb.yoj.databind.schema.Schema;
import tech.ydb.yoj.repository.db.Entity;
import tech.ydb.yoj.repository.db.EntitySchema;

import java.util.List;

import static lombok.AccessLevel.PRIVATE;

/**
* Represents a {@code view [index_name]} clause in a YQL statement, i.e. index usage
*
* @see #toYql(EntitySchema)
*/
@EqualsAndHashCode
@FieldDefaults(makeFinal = true, level = PRIVATE)
@Value
@RequiredArgsConstructor(access = PRIVATE)
public class YqlView implements YqlStatementPart<YqlView> {
public static final String TYPE = "VIEW";
public static final YqlView EMPTY = new YqlView("");

@With
@NonNull
String index;

private YqlView(String index) {
Preconditions.checkArgument(index != null, "index cannot be null");
this.index = index;
}

/**
* Creates a view clause to fetch rows using effective index
*
* @param index index
* @param index index name; must not be {@code null}
* @return view clause to fetch rows using index
*/
public static YqlView index(String index) {
public static YqlView index(@NonNull String index) {
return new YqlView(index);
}

Expand All @@ -51,7 +45,14 @@ public static YqlView empty() {
return EMPTY;
}

/**
* @deprecated This method is confusingly named, because there also is a static constructor {@link YqlView#index(String)}.
* It will be removed in YOJ 3.0.0. Please use {@link #getIndex()} instead.
*/
@Deprecated(forRemoval = true)
public String index() {
DeprecationWarnings.warnOnce("YqlView.index()",
"YqlView.index() getter will be removed in YOJ 3.0.0. Please use YqlView.getIndex() instead");
return index;
}

Expand Down Expand Up @@ -86,11 +87,6 @@ public <T extends Entity<T>> String toYql(@NonNull EntitySchema<T> schema) {
);
}

@Override
public List<? extends YqlStatementPart<?>> combine(@NonNull List<? extends YqlView> other) {
throw new UnsupportedOperationException("Multiple VIEW specifications are not supported");
}

@Override
public String toString() {
return "view [" + index + "]";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package tech.ydb.yoj.repository.ydb;

import lombok.Value;
import org.junit.Test;
import tech.ydb.yoj.repository.db.Entity;
import tech.ydb.yoj.repository.db.EntitySchema;
import tech.ydb.yoj.repository.db.RecordEntity;
import tech.ydb.yoj.repository.ydb.yql.YqlLimit;

import static java.util.Collections.singletonList;
Expand All @@ -16,7 +15,7 @@ public class YqlLimitTest {
public void nonempty_top() {
YqlLimit lim = YqlLimit.top(100L);

assertThat(lim.size()).isEqualTo(100L);
assertThat(lim.getLimit()).isEqualTo(100L);
assertThat(lim.isEmpty()).isFalse();
assertThat(lim.toYql(emptySchema)).isEqualToIgnoringCase("100");
}
Expand All @@ -30,7 +29,17 @@ public void empty_top() {
public void empty_range_without_offset() {
YqlLimit lim = YqlLimit.range(0, 0);

assertThat(lim.size()).isEqualTo(0L);
assertThat(lim).isEqualTo(YqlLimit.EMPTY);
assertThat(lim.getLimit()).isEqualTo(0L);
assertThat(lim.isEmpty()).isTrue();
assertThat(lim.toYql(emptySchema)).isEqualTo("0");
}

@Test
public void empty_range_without_offset_2() {
YqlLimit lim = YqlLimit.empty();

assertThat(lim.getLimit()).isEqualTo(0L);
assertThat(lim.isEmpty()).isTrue();
assertThat(lim.toYql(emptySchema)).isEqualTo("0");
}
Expand All @@ -39,7 +48,7 @@ public void empty_range_without_offset() {
public void nonempty_range_without_offset() {
YqlLimit lim = YqlLimit.range(0, 27);

assertThat(lim.size()).isEqualTo(27L);
assertThat(lim.getLimit()).isEqualTo(27L);
assertThat(lim.isEmpty()).isFalse();
assertThat(lim.toYql(emptySchema)).isEqualTo("27");
}
Expand All @@ -48,7 +57,8 @@ public void nonempty_range_without_offset() {
public void empty_range_with_offset() {
YqlLimit lim = YqlLimit.range(5, 5);

assertThat(lim.size()).isEqualTo(0L);
assertThat(lim).isEqualTo(YqlLimit.EMPTY);
assertThat(lim.getLimit()).isEqualTo(0L);
assertThat(lim.isEmpty()).isTrue();
assertThat(lim.toYql(emptySchema)).isEqualTo("0");
}
Expand All @@ -57,7 +67,7 @@ public void empty_range_with_offset() {
public void nonempty_range_with_offset() {
YqlLimit lim = YqlLimit.range(5, 7);

assertThat(lim.size()).isEqualTo(2L);
assertThat(lim.getLimit()).isEqualTo(2L);
assertThat(lim.isEmpty()).isFalse();
assertThat(lim.toYql(emptySchema)).isEqualTo("2 OFFSET 5");
}
Expand All @@ -78,13 +88,8 @@ public void multiple_limit_specs_are_not_supported() {
lim.combine(singletonList(YqlLimit.range(0, 200)));
}

@Value
private static final class EmptyEntity implements Entity<EmptyEntity> {
private Id id;

@Value
private static final class Id implements Entity.Id<EmptyEntity> {
int value2;
private record EmptyEntity(Id id) implements RecordEntity<EmptyEntity> {
private record Id(int value2) implements RecordEntity.Id<EmptyEntity> {
}
}
}

0 comments on commit 3e2a5d7

Please sign in to comment.