[CALCITE-7557] Linq4j.ListEnumerable.take(int) / skip(int) diverge from EnumerableDefaults on negative counts - #5124
Conversation
…om EnumerableDefaults on negative counts ListEnumerable specializes take(int) and skip(int) for lists, but unlike the adjacent BigDecimal overloads, which clamp via count.max(BigDecimal.ZERO), the int versions pass the count straight to List.subList. A negative count therefore threw IllegalArgumentException from take and IndexOutOfBoundsException from skip, while the generic EnumerableDefaults path returns an empty enumerable and the original sequence respectively. Clamp the count to zero in both methods so that the optimized list path agrees with the generic path. Math.max is used rather than negating the count so that Integer.MIN_VALUE does not overflow.
|
| new Department("Marketing", 30, ImmutableList.of(emps[1])), | ||
| }; | ||
|
|
||
| @Test void testTakeListEnumerableNegativeSize() { |
There was a problem hiding this comment.
Can this be reflected in some SQL tests as well? What happens for SELECT * LIMIT -2.5?
|
Good question — I tried it, and the answer is that negative counts can't reach this code path from SQL at all, which is why I kept the tests at the linq4j level. Three cases against So SQL is guarded twice over: the parser takes What this PR fixes is the linq4j API used directly, where |
|
I don't know if microbluey is a robot, but it would be really nice to have shorter answers. Can you please edit down the AI generated answers to the essential parts? |
|
Sorry mihaibudiu — I was asleep. I'm sorry it came across that way. Shorter answers:
|



Linq4j.asEnumerable(list)returns aListEnumerable, whose list-specializedtake(int)andskip(int)pass the count straight toList.subList. A negative count therefore throws, while the genericEnumerableDefaultspath returns an empty enumerable (take) or the original sequence (skip):Since
ListEnumerableis selected purely as an optimization forListinputs, it should be semantically equivalent to the generic path. This follows the direction agreed in the JIRA discussion: match the existingEnumerableDefaults/LINQ behaviour rather than adopt a fail-fast contract (which would be a broader change acrossEnumerableDefaults/QueryableDefaults).Fix
Clamp the count to zero in both methods. Notably, the adjacent
take(BigDecimal)/skip(BigDecimal)overloads — added in CALCITE-7624, merged after that discussion — already clamp viacount.max(BigDecimal.ZERO). So theintoverloads were the only ones in the class not clamping; this change makes them consistent with their own siblings.Math.max(count, 0)is used rather than negating the count, so thatInteger.MIN_VALUEcannot overflow.Tests
Adds the two reproducers from the JIRA case. Both assert that the generic and list-specialized paths agree.
Verification against
main:IllegalArgumentException/IndexOutOfBoundsException) and pass after. In each test the first assertion — the genericEnumerableDefaultspath — already passed before the fix, confirming this is a genuine divergence between the two paths rather than a bug in both../gradlew buildis green: 21599 tests, 0 failures, 0 errors across all modules.:linq4j:style(checkstyle + autostyle) passes.I also checked two edge cases beyond the ticket, which are not in the committed tests:
ListEnumerableEnumerableDefaultstake(Integer.MIN_VALUE)[][]skip(Integer.MIN_VALUE)[1, 2, 3][1, 2, 3]take(0)/skip(0)take(count >= size)still returnsthis, preserving the existing identity optimization.