Ruiqi Dong created CALCITE-7557:
-----------------------------------
Summary: Linq4j.ListEnumerable.take(int) / skip(int) diverge from
EnumerableDefaults on negative counts
Key: CALCITE-7557
URL: https://issues.apache.org/jira/browse/CALCITE-7557
Project: Calcite
Issue Type: Bug
Components: linq4j
Affects Versions: 1.41.0
Reporter: Ruiqi Dong
*Summary*
Linq4j.asEnumerable(list) exposes list-specialized take(int) and skip(int)
implementations that are not behaviorally equivalent to the generic
EnumerableDefaults versions for negative counts. For the same input enumerable
and the same negative count: * EnumerableDefaults.take(source, -1) returns an
empty enumerable, but ListEnumerable.take(-1) throws
* EnumerableDefaults.skip(source, -1) returns the original sequence, but
ListEnumerable.skip(-1) throws
This is a clean semantic divergence between the generic and optimized paths of
the same API.
*Affected code*
File: linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
{code:java}
public static <TSource> Enumerable<TSource> skip(Enumerable<TSource> source,
final int count) {
return skipWhile(source, (v1, v2) -> {
return v2 < count;
});
}
public static <TSource> Enumerable<TSource> take(Enumerable<TSource> source,
final int count) {
return takeWhile(
source, (v1, v2) -> {
return v2 < count;
});
} {code}
File: linq4j/src/main/java/org/apache/calcite/linq4j/Linq4j.java
{code:java}
@Override public Enumerable<T> skip(int count) {
final List<T> list = toList();
if (count >= list.size()) {
return Linq4j.emptyEnumerable();
}
return new ListEnumerable<>(list.subList(count, list.size()));
}
@Override public Enumerable<T> take(int count) {
final List<T> list = toList();
if (count >= list.size()) {
return this;
}
return new ListEnumerable<>(list.subList(0, count));
}{code}
*Reproducer*
Add the following test to
linq4j/src/test/java/org/apache/calcite/linq4j/test/Linq4jTest.java
{code:java}
@Test void testTakeListEnumerableNegativeSize() {
final List<Integer> values = Arrays.asList(1, 2, 3);
assertThat(EnumerableDefaults.take(Linq4j.asEnumerable(values), -1).toList(),
is(empty()));
assertThat(Linq4j.asEnumerable(values).take(-1).toList(), is(empty()));
}
@Test void testSkipListEnumerableNegativeSize() {
final List<Integer> values = Arrays.asList(1, 2, 3);
assertThat(EnumerableDefaults.skip(Linq4j.asEnumerable(values), -1).toList(),
is(values));
assertThat(Linq4j.asEnumerable(values).skip(-1).toList(), is(values));
}{code}
Run:
{code:java}
./gradlew :linq4j:test \
--tests
org.apache.calcite.linq4j.test.Linq4jTest.testTakeListEnumerableNegativeSize \
--tests
org.apache.calcite.linq4j.test.Linq4jTest.testSkipListEnumerableNegativeSize
{code}
Observed behavior:
The optimized list path throws in both methods instead of matching the generic
enumerable semantics
{code:java}
java.lang.IllegalArgumentException: fromIndex(0) > toIndex(-1)
at java.base/java.util.AbstractList.subListRangeCheck(AbstractList.java:509)
at java.base/java.util.AbstractList.subList(AbstractList.java:497)
at org.apache.calcite.linq4j.Linq4j$ListEnumerable.take(Linq4j.java:599)
java.lang.IndexOutOfBoundsException: fromIndex = -1
at java.base/java.util.AbstractList.subListRangeCheck(AbstractList.java:505)
at java.base/java.util.AbstractList.subList(AbstractList.java:497)
at org.apache.calcite.linq4j.Linq4j$ListEnumerable.skip(Linq4j.java:591)
{code}
Expected behavior:
Negative counts should behave the same regardless of whether the source
enumerable happens to be backed by a List. * Negative `take` should return an
empty enumerable, matching EnumerableDefaults.take
* Negative `skip` should return the original sequence, matching
EnumerableDefaults.skip
This is an optimization-path semantic regression. The same logical operation
has two implementations in Calcite, but the list-specialized path is not
behaviorally equivalent to the generic enumerable contract for negative counts.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)