Skip to content

Commit a7a7a11

Browse files
Merge branch 'main' into regex-inline-flags
2 parents cf2c2ef + aef688e commit a7a7a11

4 files changed

Lines changed: 70 additions & 44 deletions

File tree

iceberg/common/src/main/java/org/apache/iceberg/spark/source/GpuStructInternalRow.java

Lines changed: 0 additions & 25 deletions
This file was deleted.

iceberg/iceberg-1-9-x/src/main/java/com/nvidia/spark/rapids/iceberg/iceberg19x/ShimUtilsImpl.java

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,12 @@
1919
import com.nvidia.spark.rapids.RapidsConf;
2020
import com.nvidia.spark.rapids.iceberg.IcebergShimUtils;
2121
import org.apache.iceberg.*;
22-
import org.apache.iceberg.data.IdentityPartitionConverters;
2322
import org.apache.iceberg.io.FileIO;
2423
import org.apache.iceberg.io.StorageCredential;
2524
import org.apache.iceberg.io.SupportsStorageCredentials;
25+
import org.apache.iceberg.spark.SparkUtil;
2626
import org.apache.iceberg.spark.source.GpuSparkCopyOnWriteV1Scan;
2727
import org.apache.iceberg.spark.source.GpuSparkScan;
28-
import org.apache.iceberg.spark.source.GpuStructInternalRow;
29-
import org.apache.iceberg.types.Type;
3028
import org.apache.iceberg.types.Types;
3129
import org.apache.iceberg.util.PartitionUtil;
3230
import org.apache.spark.sql.connector.read.Scan;
@@ -35,33 +33,23 @@
3533
import java.util.HashMap;
3634
import java.util.Map;
3735

38-
/** Iceberg 1.9.x shim: uses {@code IdentityPartitionConverters::convertConstant}. */
36+
/** Iceberg 1.9.x shim: uses {@code SparkUtil::internalToSpark}. */
3937
public class ShimUtilsImpl implements IcebergShimUtils {
4038
@Override
4139
public String locationOf(ContentFile<?> f) {
4240
return f.location();
4341
}
4442

45-
private static Object convertConstant(Type type, Object value) {
46-
Object converted = IdentityPartitionConverters.convertConstant(type, value);
47-
if (converted instanceof StructLike && type instanceof Types.StructType) {
48-
GpuStructInternalRow row = new GpuStructInternalRow((Types.StructType) type);
49-
row.setStruct((StructLike) converted);
50-
return row;
51-
}
52-
return converted;
53-
}
54-
5543
@Override
5644
public Map<Integer, ?> constantsMap(FileScanTask task, Schema readSchema, Table table) {
5745
if (readSchema.findField(MetadataColumns.PARTITION_COLUMN_ID) != null) {
5846
Types.StructType partitionType = Partitioning.partitionType(table);
5947
return PartitionUtil.constantsMap(task,
6048
partitionType,
61-
ShimUtilsImpl::convertConstant);
49+
SparkUtil::internalToSpark);
6250
} else {
6351
return PartitionUtil.constantsMap(task,
64-
ShimUtilsImpl::convertConstant);
52+
SparkUtil::internalToSpark);
6553
}
6654
}
6755

integration_tests/src/main/python/string_test.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
from pyspark.sql.types import *
2525
import pyspark.sql.utils
2626
import pyspark.sql.functions as f
27-
from spark_session import with_cpu_session, with_gpu_session, is_databricks104_or_later, is_databricks_version_or_later, is_before_spark_320, is_spark_400_or_later, is_before_spark_340
27+
from spark_session import with_cpu_session, with_gpu_session, is_databricks104_or_later, \
28+
is_databricks_version_or_later, is_before_spark_320, is_spark_400_or_later, \
29+
is_before_spark_340, spark_version
2830

2931
_regexp_conf = { 'spark.rapids.sql.regexp.enabled': 'true' }
3032

@@ -1150,3 +1152,57 @@ def test_multi_contains_conditional(ansi):
11501152
ELSE CAST(a AS LONG)
11511153
END as result'''),
11521154
conf = conf)
1155+
1156+
1157+
# SPARK-57507 / #15383: reverse must clamp truncated trailing UTF-8 to the row boundary.
1158+
# Neighbor sentinel rows make an over-read observable.
1159+
def _cpu_reverse_has_spark_57507():
1160+
version = spark_version()
1161+
# The fix was backported to the 4.0.4, 4.1.3, and 4.2.0 release lines.
1162+
return ('4.0.4' <= version < '4.1.0'
1163+
or '4.1.3' <= version < '4.2.0'
1164+
or version >= '4.2.0')
1165+
1166+
1167+
@ignore_order(local=True)
1168+
@pytest.mark.parametrize('hex_in,expected_hex', [
1169+
('41CE', 'CE41'), # A + truncated 2-byte lead
1170+
('41E4B8', 'E4B841'), # A + truncated 3-byte lead
1171+
('41F090', 'F09041'), # A + truncated 4-byte lead
1172+
('E4B896CE', 'CEE4B896'), # complete 世 + orphan 2-byte lead
1173+
('41C3A9', 'C3A941'), # well-formed 2-byte control
1174+
('41E4B896', 'E4B89641'), # well-formed 3-byte control
1175+
('41F0908D88', 'F0908D8841'), # well-formed 4-byte control
1176+
], ids=idfn)
1177+
def test_reverse_truncated_trailing_utf8(hex_in, expected_hex):
1178+
def do_it(spark):
1179+
# Keep target + sentinel neighbors adjacent in one partition so an over-read
1180+
# into the next row is observable. Avoid ORDER BY (CPU shuffle) and unhex
1181+
# (not allowed in GpuCpuBridge); feed raw bytes via BINARY then cast to STRING.
1182+
df = spark.createDataFrame(
1183+
[
1184+
(0, bytearray.fromhex(hex_in)),
1185+
(1, bytearray.fromhex('FFFFFFFF')),
1186+
(2, bytearray.fromhex('414243')),
1187+
],
1188+
'row_id INT, b BINARY'
1189+
).coalesce(1).selectExpr('row_id', 'CAST(b AS STRING) AS v')
1190+
return df.selectExpr('row_id', 'hex(reverse(v)) as h')
1191+
1192+
if _cpu_reverse_has_spark_57507():
1193+
assert_cpu_and_gpu_are_equal_collect_with_capture(
1194+
do_it, exist_classes='GpuReverse', require_non_empty=True)
1195+
else:
1196+
# Older CPU builds over-read malformed trailing UTF-8, so validate the GPU result
1197+
# against Spark's corrected semantics instead of asserting CPU/GPU equality.
1198+
def collect_gpu(spark):
1199+
out = do_it(spark)
1200+
rows = out.collect()
1201+
plan = out._jdf.queryExecution().executedPlan().toString()
1202+
# Expression trees render as lowercase "gpureverse(...)", not the class name.
1203+
assert 'gpureverse' in plan.lower(), 'expected GpuReverse in plan, got:\n' + plan
1204+
return rows
1205+
1206+
rows = with_gpu_session(collect_gpu)
1207+
rows_by_id = {row['row_id']: row['h'] for row in rows}
1208+
assert rows_by_id[0] == expected_hex

sql-plugin/src/main/scala/org/apache/spark/sql/rapids/collectionOperations.scala

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import com.nvidia.spark.rapids.ArrayIndexUtils.firstIndexAndNumElementUnchecked
2626
import com.nvidia.spark.rapids.BoolUtils.isAllValidTrue
2727
import com.nvidia.spark.rapids.GpuListUtils
2828
import com.nvidia.spark.rapids.RapidsPluginImplicits._
29-
import com.nvidia.spark.rapids.jni.{GpuListSliceUtils, MapUtils}
29+
import com.nvidia.spark.rapids.jni.{GpuListSliceUtils, MapUtils, StringUtils}
3030
import com.nvidia.spark.rapids.shims.{GetSequenceSize, NullIntolerantShim, ShimExpression}
3131

3232
import org.apache.spark.sql.catalyst.analysis.{TypeCheckResult, TypeCoercion}
@@ -674,7 +674,14 @@ case class GpuReverse(child: Expression) extends GpuUnaryExpression {
674674
override def dataType: DataType = child.dataType
675675

676676
override protected def doColumnar(input: GpuColumnVector): ColumnVector = {
677-
input.getBase.reverseStringsOrLists()
677+
// Strings use Spark UTF8String.reverse semantics (SPARK-57507): clamp truncated trailing
678+
// multi-byte UTF-8 widths to the bytes remaining in each row. libcudf reverse can
679+
// over-read into the next row for malformed Spark StringType values.
680+
if (child.dataType.isInstanceOf[StringType]) {
681+
StringUtils.reverseStrings(input.getBase)
682+
} else {
683+
input.getBase.reverseStringsOrLists()
684+
}
678685
}
679686
}
680687

0 commit comments

Comments
 (0)