Skip to content

Commit 6e6fb8f

Browse files
Add create table as select support for iceberg. (#13595)
1 parent a710b4d commit 6e6fb8f

6 files changed

Lines changed: 470 additions & 23 deletions

File tree

iceberg/src/main/scala/com/nvidia/spark/rapids/iceberg/IcebergProviderImpl.scala

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,17 @@ package com.nvidia.spark.rapids.iceberg
1919
import scala.reflect.ClassTag
2020
import scala.util.{Failure, Success, Try}
2121

22-
import com.nvidia.spark.rapids.{AppendDataExecMeta, FileFormatChecks, GpuExec, GpuExpression, GpuRowToColumnarExec, GpuScan, IcebergFormatType, RapidsConf, ReadFileOp, ScanMeta, ScanRule, ShimReflectionUtils, StaticInvokeMeta, TargetSize, WriteFileOp}
22+
import com.nvidia.spark.rapids.{AppendDataExecMeta, AtomicCreateTableAsSelectExecMeta, FileFormatChecks, GpuExec, GpuExpression, GpuRowToColumnarExec, GpuScan, IcebergFormatType, RapidsConf, ReadFileOp, ScanMeta, ScanRule, ShimReflectionUtils, StaticInvokeMeta, TargetSize, WriteFileOp}
2323
import org.apache.iceberg.spark.functions.{BucketFunction, GpuBucketExpression}
2424
import org.apache.iceberg.spark.source.{GpuSparkBatchQueryScan, GpuSparkWrite}
25+
import org.apache.iceberg.spark.supportsCatalog
2526

2627
import org.apache.spark.sql.catalyst.expressions.objects.StaticInvoke
2728
import org.apache.spark.sql.connector.read.Scan
2829
import org.apache.spark.sql.connector.write.Write
2930
import org.apache.spark.sql.execution.SparkPlan
30-
import org.apache.spark.sql.execution.datasources.v2.{AppendDataExec, GpuAppendDataExec}
31+
import org.apache.spark.sql.execution.datasources.v2.{AppendDataExec, AtomicCreateTableAsSelectExec, GpuAppendDataExec}
32+
import org.apache.spark.sql.execution.datasources.v2.rapids.GpuAtomicCreateTableAsSelectExec
3133

3234
class IcebergProviderImpl extends IcebergProvider {
3335
override def getScans: Map[Class[_ <: Scan], ScanRule[_ <: Scan]] = {
@@ -98,6 +100,41 @@ class IcebergProviderImpl extends IcebergProvider {
98100
GpuSparkWrite.supports(write)
99101
}
100102

103+
override def isSupportedCatalog(catalogClass: Class[_]): Boolean = {
104+
supportsCatalog(catalogClass)
105+
}
106+
107+
override def tagForGpu(
108+
cpuExec: AtomicCreateTableAsSelectExec,
109+
meta: AtomicCreateTableAsSelectExecMeta): Unit = {
110+
if (!meta.conf.isIcebergEnabled) {
111+
meta.willNotWorkOnGpu("Iceberg input and output has been disabled. To enable set " +
112+
s"${RapidsConf.ENABLE_ICEBERG.key} to true")
113+
}
114+
115+
if (!meta.conf.isIcebergWriteEnabled) {
116+
meta.willNotWorkOnGpu("Iceberg output has been disabled. To enable set " +
117+
s"${RapidsConf.ENABLE_ICEBERG_WRITE.key} to true")
118+
}
119+
120+
FileFormatChecks.tag(meta, cpuExec.query.schema, IcebergFormatType, WriteFileOp)
121+
122+
GpuSparkWrite.tagForGpuCtas(cpuExec, meta)
123+
}
124+
125+
override def convertToGpu(
126+
cpuExec: AtomicCreateTableAsSelectExec,
127+
meta: AtomicCreateTableAsSelectExecMeta): GpuExec = {
128+
GpuAtomicCreateTableAsSelectExec(
129+
cpuExec.catalog,
130+
cpuExec.ident,
131+
cpuExec.partitioning,
132+
cpuExec.query,
133+
cpuExec.tableSpec,
134+
cpuExec.writeOptions,
135+
cpuExec.ifNotExists)
136+
}
137+
101138
override def tagForGpu(cpuExec: AppendDataExec, meta: AppendDataExecMeta): Unit = {
102139
if (!meta.conf.isIcebergEnabled) {
103140
meta.willNotWorkOnGpu("Iceberg input and output has been disabled. To enable set " +
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) 2025, NVIDIA CORPORATION.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.apache.iceberg
18+
19+
package object spark {
20+
def supportsCatalog(catalogClass: Class[_]): Boolean = {
21+
classOf[BaseCatalog].isAssignableFrom(catalogClass)
22+
}
23+
}

iceberg/src/main/scala/org/apache/iceberg/spark/source/GpuSparkWrite.scala

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import com.nvidia.spark.rapids.fileio.iceberg.IcebergFileIO
2626
import com.nvidia.spark.rapids.iceberg.GpuIcebergPartitioner
2727
import org.apache.hadoop.mapreduce.Job
2828
import org.apache.hadoop.shaded.org.apache.commons.lang3.reflect.{FieldUtils, MethodUtils}
29-
import org.apache.iceberg.{DataFile, FileFormat, PartitionSpec, Schema, SerializableTable, SnapshotUpdate, Table}
29+
import org.apache.iceberg.{DataFile, FileFormat, PartitionSpec, Schema, SerializableTable, SnapshotUpdate, Table, TableProperties}
3030
import org.apache.iceberg.io.{DataWriteResult, FileIO, GpuClusteredDataWriter, GpuFanoutDataWriter, GpuRollingDataWriter, OutputFileFactory, PartitioningWriter}
31+
import org.apache.iceberg.spark.{Spark3Util, SparkSchemaUtil}
3132
import org.apache.iceberg.spark.functions.{GpuFieldTransform, GpuTransform}
3233
import org.apache.iceberg.spark.source.SparkWrite.TaskCommit
3334

@@ -39,6 +40,7 @@ import org.apache.spark.sql.connector.distributions.Distribution
3940
import org.apache.spark.sql.connector.expressions.SortOrder
4041
import org.apache.spark.sql.connector.write.{BatchWrite, DataWriter, DataWriterFactory, RequiresDistributionAndOrdering, Write, WriterCommitMessage}
4142
import org.apache.spark.sql.connector.write.streaming.StreamingWrite
43+
import org.apache.spark.sql.execution.datasources.v2.AtomicCreateTableAsSelectExec
4244
import org.apache.spark.sql.rapids.GpuWriteJobStatsTracker
4345
import org.apache.spark.sql.types.StructType
4446
import org.apache.spark.sql.vectorized.ColumnarBatch
@@ -141,28 +143,30 @@ object GpuSparkWrite {
141143
classOf[SparkWrite].isAssignableFrom(cpuClass)
142144
}
143145

144-
def tagForGpu(cpuWrite: Write, meta: SparkPlanMeta[_]): Unit = {
145-
if (!supports(cpuWrite.getClass)) {
146-
meta.willNotWorkOnGpu(s"GpuSparkWrite only supports ${classOf[SparkWrite].getName}, " +
147-
s"but got: ${cpuWrite.getClass.getName}")
148-
return
149-
}
150-
151-
val dataFileFormat: FileFormat = FieldUtils.readField(cpuWrite, "format", true)
152-
.asInstanceOf[FileFormat]
153-
146+
/**
147+
* Tag for GPU support for Iceberg write operations.
148+
* This method checks:
149+
* 1. File format is supported (only Parquet)
150+
* 2. Partition transforms are supported
151+
*
152+
* @param dataFileFormat The file format for the write operation
153+
* @param partitionSpec The partition specification
154+
* @param dsSchema The DataFrame schema
155+
* @param writeSchema The write schema
156+
* @param meta The metadata for tagging
157+
*/
158+
private def tagForGpuWrite(
159+
dataFileFormat: FileFormat,
160+
partitionSpec: PartitionSpec,
161+
dsSchema: StructType,
162+
writeSchema: Schema,
163+
meta: SparkPlanMeta[_]): Unit = {
164+
// Check file format support
154165
if (!dataFileFormat.equals(FileFormat.PARQUET)) {
155166
meta.willNotWorkOnGpu(s"GpuSparkWrite only supports Parquet, but got: $dataFileFormat")
156167
}
157168

158-
val table: Table = FieldUtils.readField(cpuWrite, "table", true).asInstanceOf[Table]
159-
val partitionSpec = table.spec()
160-
161-
val dsSchema = FieldUtils.readField(cpuWrite, "dsSchema", true)
162-
.asInstanceOf[StructType]
163-
val writeSchema = FieldUtils.readField(cpuWrite, "writeSchema", true)
164-
.asInstanceOf[Schema]
165-
169+
// Check partition transform support
166170
if (partitionSpec.isPartitioned) {
167171
for (partitionField <- partitionSpec.fields().asScala) {
168172
val transform = partitionField.transform()
@@ -180,6 +184,55 @@ object GpuSparkWrite {
180184
}
181185
}
182186

187+
def tagForGpu(cpuWrite: Write, meta: SparkPlanMeta[_]): Unit = {
188+
if (!supports(cpuWrite.getClass)) {
189+
meta.willNotWorkOnGpu(s"GpuSparkWrite only supports ${classOf[SparkWrite].getName}, " +
190+
s"but got: ${cpuWrite.getClass.getName}")
191+
return
192+
}
193+
194+
val dataFileFormat: FileFormat = FieldUtils.readField(cpuWrite, "format", true)
195+
.asInstanceOf[FileFormat]
196+
197+
val table: Table = FieldUtils.readField(cpuWrite, "table", true).asInstanceOf[Table]
198+
val partitionSpec = table.spec()
199+
200+
val dsSchema = FieldUtils.readField(cpuWrite, "dsSchema", true)
201+
.asInstanceOf[StructType]
202+
val writeSchema = FieldUtils.readField(cpuWrite, "writeSchema", true)
203+
.asInstanceOf[Schema]
204+
205+
tagForGpuWrite(dataFileFormat, partitionSpec, dsSchema, writeSchema, meta)
206+
}
207+
208+
/**
209+
* Tag for GPU support for Iceberg CTAS operations.
210+
* This method checks file format and partitioning support for CREATE TABLE AS SELECT.
211+
*
212+
* @param cpuExec The CPU AtomicCreateTableAsSelectExec
213+
* @param meta The metadata for tagging
214+
*/
215+
def tagForGpuCtas(
216+
cpuExec: AtomicCreateTableAsSelectExec,
217+
meta: SparkPlanMeta[_]): Unit = {
218+
val properties: Map[String, String] = Spark3Util
219+
.rebuildCreateProperties(cpuExec.tableSpec.properties.asJava)
220+
.asScala.toMap
221+
val fileFormatStr = properties.getOrElse(TableProperties.DEFAULT_FILE_FORMAT,
222+
TableProperties.DEFAULT_FILE_FORMAT_DEFAULT)
223+
224+
val fileFormat = FileFormat.fromString(fileFormatStr)
225+
226+
// Convert Spark schema to Iceberg schema
227+
val querySchema = cpuExec.query.schema
228+
val icebergSchema = SparkSchemaUtil.convert(querySchema)
229+
230+
// Convert Spark connector transforms to Iceberg PartitionSpec
231+
val partitionSpec = Spark3Util.toPartitionSpec(icebergSchema, cpuExec.partitioning.toArray)
232+
233+
// Reuse tagForGpuWrite for validation
234+
tagForGpuWrite(fileFormat, partitionSpec, querySchema, icebergSchema, meta)
235+
}
183236

184237
def convert(cpuWrite: Write): GpuSparkWrite = {
185238
new GpuSparkWrite(cpuWrite.asInstanceOf[SparkWrite])

0 commit comments

Comments
 (0)