Skip to content

Commit 55ddfe3

Browse files
aknayarcloud-fan
authored andcommitted
[SPARK-56567][SQL] Fix array_insert Int.MinValue pos overflow
### What changes were proposed in this pull request? This PR fixes `array_insert` to raise the documented `COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION` error when called with `pos = Int.MinValue`, instead of leaking an internal JVM exception. Below is a minimalistic repro for the bug: ``` from pyspark.sql import SparkSession spark = SparkSession.builder.appName("ArrayInsertIntMinTest").getOrCreate() def run(query): print(query) try: spark.sql(query).show() except Exception as e: print(f"> {type(e).__name__}: {str(e).splitlines()[0]}") print() print("Correctly throws COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION") run("SELECT array_insert(array(1), -2147483647, 3)") print("Incorrectly throws ArrayIndexOutOfBoundsException") run("SELECT array_insert(array(1), -2147483648, 3)") spark.stop() ``` `array_insert` should raise `SparkRuntimeException` with condition `COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION` here. However, it instead crashes with an internal JVM exception: `java.lang.AssertionError` from `UnsafeArrayData.setInt(-2147483646, ...)` under whole-stage codegen, or `java.lang.ArrayIndexOutOfBoundsException: Index -2147483646 out of bounds for length 2` in interpreted mode. This is because `ArrayInsert` does `int` arithmetic on `pos` (e.g. `-pos`, `java.lang.Math.abs(pos)`), which silently wraps back to `Int.MinValue`. The `newPosExtendsArrayLeft` check therefore returns the wrong answer, the code takes the wrong branch, never reaches the `MAX_ROUNDED_ARRAY_LENGTH` guard, and computes a garbage index that crashes later. The fix is to widen `pos` arithmetic to `Long` in both `ArrayInsert.nullSafeEval` (interpreted) and `ArrayInsert.doGenCode` (codegen). The existing `MAX_ROUNDED_ARRAY_LENGTH` check then correctly catches the oversized result on the right branch. ### Why are the changes needed? There is currently an error-reporting bug. `array_insert(arr, Int.MinValue, item)` fails with an internal JVM exception (`AssertionError` or `ArrayIndexOutOfBoundsException`) instead of the documented `COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION` error. ### Does this PR introduce _any_ user-facing change? Yes, for `pos = Int.MinValue` with a non-empty input array, `array_insert` now throws `SparkRuntimeException` with condition `COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION` instead of leaking an internal `AssertionError` / `ArrayIndexOutOfBoundsException`. This matches the behavior already in place for neighboring positions like `Int.MinValue + 1`. ### How was this patch tested? Added unit tests to `CollectionExpressionsSuite` to validate correct error reporting when `pos = Int.MinValue`. ### Was this patch authored or co-authored using generative AI tooling? Generated-by: claude-4.7-opus-high Closes apache#55476 from aknayar/array-insert-pos-fix. Authored-by: Akash Nayar <akashknayar5@gmail.com> Signed-off-by: Wenchen Fan <wenchen@databricks.com>
1 parent f519b2d commit 55ddfe3

2 files changed

Lines changed: 45 additions & 18 deletions

File tree

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/collectionOperations.scala

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5126,52 +5126,55 @@ case class ArrayInsert(
51265126

51275127
new GenericArrayData(newArray)
51285128
} else {
5129-
var posInt = pos.asInstanceOf[Int]
5130-
if (posInt == 0) {
5129+
// Widen `pos` to Long to avoid overflow (e.g. `-Int.MinValue` wraps back to `Int.MinValue`).
5130+
var posLong: Long = pos.asInstanceOf[Int].toLong
5131+
if (posLong == 0L) {
51315132
throw QueryExecutionErrors.invalidIndexOfZeroError(getContextOrNull())
51325133
}
51335134

5134-
val newPosExtendsArrayLeft = (posInt < 0) && (-posInt > baseArr.numElements())
5135+
val newPosExtendsArrayLeft = (posLong < 0) && (-posLong > baseArr.numElements())
51355136

51365137
if (newPosExtendsArrayLeft) {
5137-
val baseOffset = if (legacyNegativeIndex) 1 else 0
5138+
val baseOffset: Long = if (legacyNegativeIndex) 1L else 0L
51385139
// special case- if the new position is negative but larger than the current array size
51395140
// place the new item at start of array, place the current array contents at the end
51405141
// and fill the newly created array elements inbetween with a null
51415142

5142-
val newArrayLength = -posInt + baseOffset
5143+
val newArrayLength: Long = -posLong + baseOffset
51435144

51445145
if (newArrayLength > ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH) {
51455146
throw QueryExecutionErrors.arrayFunctionWithElementsExceedLimitError(
51465147
prettyName, newArrayLength)
51475148
}
51485149

5149-
val newArray = new Array[Any](newArrayLength)
5150+
val newArray = new Array[Any](newArrayLength.toInt)
51505151

51515152
baseArr.foreach(elementType, (i, v) => {
51525153
// current position, offset by new item + new null array elements
5153-
val elementPosition = i + baseOffset + math.abs(posInt + baseArr.numElements())
5154+
val elementPosition =
5155+
(i + baseOffset + math.abs(posLong + baseArr.numElements())).toInt
51545156
newArray(elementPosition) = v
51555157
})
51565158

51575159
newArray(0) = item
51585160

51595161
new GenericArrayData(newArray)
51605162
} else {
5161-
if (posInt < 0) {
5162-
posInt = posInt + baseArr.numElements() + (if (legacyNegativeIndex) 0 else 1)
5163-
} else if (posInt > 0) {
5164-
posInt = posInt - 1
5163+
if (posLong < 0) {
5164+
posLong = posLong + baseArr.numElements() + (if (legacyNegativeIndex) 0 else 1)
5165+
} else if (posLong > 0) {
5166+
posLong = posLong - 1
51655167
}
51665168

5167-
val newArrayLength = math.max(baseArr.numElements() + 1, posInt + 1)
5169+
val newArrayLength: Long = math.max(baseArr.numElements() + 1L, posLong + 1L)
51685170

51695171
if (newArrayLength > ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH) {
51705172
throw QueryExecutionErrors.arrayFunctionWithElementsExceedLimitError(
51715173
prettyName, newArrayLength)
51725174
}
51735175

5174-
val newArray = new Array[Any](newArrayLength)
5176+
val newArray = new Array[Any](newArrayLength.toInt)
5177+
val posInt = posLong.toInt
51755178

51765179
baseArr.foreach(elementType, (i, v) => {
51775180
if (i >= posInt) {
@@ -5238,23 +5241,29 @@ case class ArrayInsert(
52385241
} else {
52395242
val pos = posExpr.value
52405243
val baseOffset = if (legacyNegativeIndex) 1 else 0
5244+
// Widen `pos` arithmetic to long so that `Int.MIN_VALUE` doesn't silently overflow
5245+
// (e.g. `Math.abs(Int.MIN_VALUE)` returns `Int.MIN_VALUE`).
5246+
val posLong = ctx.freshName("posLong")
5247+
val resLengthLong = ctx.freshName("resLengthLong")
52415248
s"""
5249+
|long $posLong = (long) $pos;
52425250
|int $itemInsertionIndex = 0;
52435251
|int $resLength = 0;
52445252
|int $adjustedAllocIdx = 0;
52455253
|boolean $insertedItemIsNull = ${itemExpr.isNull};
52465254
|
5247-
|if ($pos == 0) {
5255+
|if ($posLong == 0L) {
52485256
| throw QueryExecutionErrors.invalidIndexOfZeroError($errorContext);
52495257
|}
52505258
|
5251-
|if ($pos < 0 && (java.lang.Math.abs($pos) > $arr.numElements())) {
5259+
|if ($posLong < 0L && (-$posLong > $arr.numElements())) {
52525260
|
5253-
| $resLength = java.lang.Math.abs($pos) + $baseOffset;
5254-
| if ($resLength > ${ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH}) {
5261+
| long $resLengthLong = -$posLong + ${baseOffset}L;
5262+
| if ($resLengthLong > ${ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH}) {
52555263
| throw QueryExecutionErrors.arrayFunctionWithElementsExceedLimitError(
5256-
| "$prettyName", $resLength);
5264+
| "$prettyName", $resLengthLong);
52575265
| }
5266+
| $resLength = (int) $resLengthLong;
52585267
|
52595268
| $allocation
52605269
| for (int $i = 0; $i < $arr.numElements(); $i ++) {

sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/CollectionExpressionsSuite.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3279,4 +3279,22 @@ class CollectionExpressionsSuite
32793279
a, Literal(5), Literal.create("q", StringType)), Seq("b", "a", "c", null, "q")
32803280
)
32813281
}
3282+
3283+
test("SPARK-56567: Array insert with pos = Int.MinValue") {
3284+
val a = Literal.create(Seq(1, 2, 4), ArrayType(IntegerType))
3285+
checkErrorInExpression[SparkRuntimeException](
3286+
ArrayInsert(a, Literal(Int.MinValue), Literal(3), legacyNegativeIndex = false),
3287+
condition = "COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION",
3288+
parameters = Map(
3289+
"numberOfElements" -> (-BigInt(Int.MinValue)).toString,
3290+
"maxRoundedArrayLength" -> ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH.toString,
3291+
"functionName" -> "`array_insert`"))
3292+
checkErrorInExpression[SparkRuntimeException](
3293+
ArrayInsert(a, Literal(Int.MinValue), Literal(3), legacyNegativeIndex = true),
3294+
condition = "COLLECTION_SIZE_LIMIT_EXCEEDED.FUNCTION",
3295+
parameters = Map(
3296+
"numberOfElements" -> (-BigInt(Int.MinValue) + 1).toString,
3297+
"maxRoundedArrayLength" -> ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH.toString,
3298+
"functionName" -> "`array_insert`"))
3299+
}
32823300
}

0 commit comments

Comments
 (0)