Repository navigation
feat: enable native Iceberg writes after Spark required-field validation #7155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e758865
32bd3e2
9737fc3
b5af037
840e22f
41d12ac
d9e1fb7
b77bf16
54a7aee
867334a
1ff2712
4ce2f72
778b210
4678792
7dcbf1f
881d0dc
420f36b
d6b08d2
ca520d8
c7ac767
6e850d7
46e5ab6
f541b9d
ae118f6
f150011
750f89b
c6739bd
94389a5
f66d9ec
ea49c17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,18 +27,21 @@ import org.apache.arrow.vector.ipc.ArrowReader | |
| import org.apache.spark.broadcast.Broadcast | ||
| import org.apache.spark.rdd.RDD | ||
| import org.apache.spark.sql.catalyst.InternalRow | ||
| import org.apache.spark.sql.catalyst.expressions.{Attribute, SortOrder} | ||
| import org.apache.spark.sql.catalyst.expressions.{Alias, Attribute, AttributeSet, CreateNamedStruct, Expression, GetStructField, If, Literal, MapFromArrays, MapKeys, SortOrder} | ||
| import org.apache.spark.sql.catalyst.expressions.objects.AssertNotNull | ||
| import org.apache.spark.sql.catalyst.plans.physical.Partitioning | ||
| import org.apache.spark.sql.comet.execution.arrow.{CometArrowStream, CometNativeArrowSource, RowArrowReader, SparkColumnarArrowReader} | ||
| import org.apache.spark.sql.comet.util.Utils | ||
| import org.apache.spark.sql.execution.{RowToColumnarTransition, SparkPlan} | ||
| import org.apache.spark.sql.execution.{CoalesceExec, CollectLimitExec, ColumnarToRowTransition, FilterExec, GlobalLimitExec, LocalLimitExec, ProjectExec, RowToColumnarTransition, SortExec, SparkPlan} | ||
| import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics} | ||
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.sql.vectorized.ColumnarBatch | ||
| import org.apache.spark.unsafe.types.UTF8String | ||
|
|
||
| import org.apache.comet.{CometConf, DataTypeSupport} | ||
| import org.apache.comet.serde.OperatorOuterClass | ||
| import org.apache.comet.serde.operator.CometSink | ||
| import org.apache.comet.shims.CometTypeShim | ||
|
|
||
| case class CometSparkToColumnarExec(child: SparkPlan, batchSize: Option[Int] = None) | ||
| extends RowToColumnarTransition | ||
|
|
@@ -131,6 +134,77 @@ case class CometSparkToColumnarExec(child: SparkPlan, batchSize: Option[Int] = N | |
|
|
||
| object CometSparkToColumnarExec extends CometSink[SparkPlan] with DataTypeSupport { | ||
|
|
||
| private object RowTypeSupport extends DataTypeSupport with CometTypeShim | ||
|
|
||
| // RowArrowReader recursively writes the collection types supported by DataTypeSupport. | ||
| def isRowSchemaSupported(schema: StructType, fallbackReasons: ListBuffer[String]): Boolean = | ||
| RowTypeSupport.isSchemaSupported(schema, fallbackReasons) | ||
|
Comment on lines
+137
to
+141
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’ve added readback tests against the Java writer for nested arrays, struct elements, and map values, including nulls and empty collections, using small batches with AQE on and off. |
||
|
|
||
| /** | ||
| * Whether the Iceberg writer may convert this Spark row subtree to Arrow. Native import | ||
| * normalizes invalid UTF-8, while Spark rows can retain those bytes. A new conversion is safe | ||
| * only when its string outputs preserve already valid strings or contain validated literals. | ||
| */ | ||
| def isRowConversionSupported(plan: SparkPlan, fallbackReasons: ListBuffer[String]): Boolean = { | ||
| if (!isRowSchemaSupported(plan.schema, fallbackReasons)) { | ||
| false | ||
| } else if (hasValidStrings(plan)) { | ||
| true | ||
| } else { | ||
| fallbackReasons += "Iceberg writer row conversion cannot prove that string outputs contain " + | ||
| "valid UTF-8; native Arrow import could change their bytes" | ||
| false | ||
| } | ||
| } | ||
|
|
||
| private def containsString(dt: DataType): Boolean = | ||
| dt.existsRecursively(_.isInstanceOf[StringType]) | ||
|
|
||
| private def hasValidStrings(plan: SparkPlan): Boolean = { | ||
| if (!containsString(plan.schema)) { | ||
| true | ||
| } else { | ||
| plan match { | ||
| // These planning placeholders disappear without running a native import. | ||
| case CometScanWrapper(_, source) => hasValidStrings(source) | ||
| case CometSinkPlaceHolder(_, _, child) => hasValidStrings(child) | ||
| case _: CometNativeExec => true | ||
| case project: ProjectExec => | ||
| hasValidStrings(project.child) && project.projectList.forall( | ||
| preservesValidStrings(_, project.child.outputSet)) | ||
| case _: SortExec | _: LocalLimitExec | _: GlobalLimitExec | _: CollectLimitExec | | ||
| _: FilterExec | _: CoalesceExec | _: ColumnarToRowTransition => | ||
| plan.children.forall(hasValidStrings) | ||
| case _ => false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private def preservesValidStrings(expr: Expression, input: AttributeSet): Boolean = { | ||
| if (!containsString(expr.dataType)) { | ||
| true | ||
| } else { | ||
| expr match { | ||
| case attr: Attribute => input.contains(attr) | ||
| case alias: Alias => preservesValidStrings(alias.child, input) | ||
| case assertion: AssertNotNull => preservesValidStrings(assertion.child, input) | ||
| case field: GetStructField => preservesValidStrings(field.child, input) | ||
| case struct: CreateNamedStruct => struct.valExprs.forall(preservesValidStrings(_, input)) | ||
| // Spark aligns struct-valued maps by rebuilding them with their existing keys. | ||
| case keys: MapKeys => preservesValidStrings(keys.child, input) | ||
| case map: MapFromArrays => | ||
| preservesValidStrings(map.left, input) && preservesValidStrings(map.right, input) | ||
| case conditional: If => | ||
| preservesValidStrings(conditional.trueValue, input) && | ||
| preservesValidStrings(conditional.falseValue, input) | ||
| case Literal(null, _) => true | ||
| case Literal(value: UTF8String, _: StringType) => RowTypeSupport.isValidUtf8(value) | ||
| // String-bearing collection transforms and other producers need their own proof. | ||
| case _ => false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override def createExec( | ||
| nativeOp: OperatorOuterClass.Operator, | ||
| op: SparkPlan): CometNativeExec = { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell, these two declines only decide which of two errors a failing write reports, and I could not find a case where committed data differs. Native input with
fixed(N)already stays eligible and reports Arrow's cast error where iceberg-java reports Parquet's. If error identity is part of the contract here, could the description say so? Otherwise dropping both would remove the reflection chain inisRowInputEvaluationSupported(which repeats lines 1122 to 1174),hasMaterializedRowInputand two of the longest tests.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve kept both guards and documented error precedence, exception type, and message preservation for the new row bridge only. Existing Arrow-native writes are outside this guarantee.