Skip to content

Commit cd37d7a

Browse files
[VL] Fall back to vanilla Delta write for unsupported schemas (#12444)
Velox cannot write every Spark type. When native Delta write is enabled, Gluten offloads certain Delta write commands to the native writer via OffloadDeltaCommand -- DataFrameWriter.save (append/overwrite), CREATE/REPLACE TABLE AS SELECT, UPDATE, DELETE and OPTIMIZE. The native write path inserts a RowToVeloxColumnarExec transition whose SparkArrowUtil.toArrowSchema call throws `UnsupportedOperationException: Unsupported data type: <type>` at runtime for any type it has no Arrow mapping for (VariantType is the motivating example). (Plain SQL INSERT INTO and MERGE are not offloaded and were unaffected.) Guard GlutenOptimisticTransaction.writeFiles with the backend's existing schema validator: if VeloxValidatorApi.validateSchema reports the input schema is not supported, delegate to super.writeFiles (the vanilla Delta write path) instead of offloading. This reuses the same check Velox already applies to scan schemas, so unsupported types fall back consistently before Arrow schema conversion rather than adding a one-off VariantType guard. Supported writes are unaffected, and the validator's reason is logged when it falls back. Adds DeltaVariantWriteSuite, which exercises offloaded writes: top-level and struct-nested variant columns via DataFrameWriter.save, plus an UPDATE.
1 parent 022bc4b commit cd37d7a

2 files changed

Lines changed: 108 additions & 1 deletion

File tree

backends-velox/src-delta40/main/scala/org/apache/spark/sql/delta/GlutenOptimisticTransaction.scala

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*/
1717
package org.apache.spark.sql.delta
1818

19-
import org.apache.gluten.backendsapi.velox.VeloxBatchType
19+
import org.apache.gluten.backendsapi.velox.{VeloxBatchType, VeloxValidatorApi}
2020
import org.apache.gluten.extension.columnar.transition.Transitions
2121

2222
import org.apache.spark.sql.{AnalysisException, Dataset}
@@ -48,6 +48,19 @@ class GlutenOptimisticTransaction(delegate: OptimisticTransaction)
4848
writeOptions: Option[DeltaOptions],
4949
isOptimize: Boolean,
5050
additionalConstraints: Seq[Constraint]): Seq[FileAction] = {
51+
// Velox cannot write every Spark type: the native write path inserts a RowToVeloxColumnarExec
52+
// transition whose SparkArrowUtil.toArrowSchema call throws (e.g. `Unsupported data type:
53+
// variant`) for any type it has no Arrow mapping for. Reuse the backend's schema validator so
54+
// such writes fall back to the vanilla Delta write path instead of failing at runtime.
55+
val unsupportedReason = VeloxValidatorApi.validateSchema(inputData.schema)
56+
unsupportedReason match {
57+
case Some(reason) =>
58+
logInfo(
59+
s"Schema is not supported by Velox ($reason); falling back to the " +
60+
"vanilla Delta write path.")
61+
return super.writeFiles(inputData, writeOptions, isOptimize, additionalConstraints)
62+
case None =>
63+
}
5164
hasWritten = true
5265

5366
val spark = inputData.sparkSession
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.spark.sql.delta
18+
19+
import org.apache.spark.sql.QueryTest
20+
import org.apache.spark.sql.Row
21+
import org.apache.spark.sql.delta.test.DeltaSQLCommandTest
22+
import org.apache.spark.sql.test.SharedSparkSession
23+
24+
/**
25+
* Velox has no Arrow representation for VariantType, so a Delta write that Gluten offloads to the
26+
* native columnar writer (DataFrameWriter.save, UPDATE, ...) used to throw
27+
* `UnsupportedOperationException: Unsupported data type: variant` at runtime.
28+
* GlutenOptimisticTransaction now detects a schema Velox cannot write (such as one containing a
29+
* variant) and delegates such writes to the vanilla Delta write path.
30+
*
31+
* These tests use write commands that Gluten targets for native offload (DataFrameWriter.save and
32+
* UPDATE): without the fix they reach the native path and fail; with the fix they fall back to
33+
* vanilla Delta and succeed. Plain INSERT INTO is never offloaded, so it would pass regardless of
34+
* the fix and is intentionally not used here.
35+
*/
36+
class DeltaVariantWriteSuite
37+
extends QueryTest
38+
with SharedSparkSession
39+
with DeltaSQLCommandTest {
40+
41+
test("write and read a top-level variant column") {
42+
withTempDir {
43+
dir =>
44+
val path = dir.getCanonicalPath
45+
spark
46+
.range(3)
47+
.selectExpr("'foo' AS s", "parse_json(cast(id + 99 AS string)) AS v")
48+
.write
49+
.format("delta")
50+
.mode("overwrite")
51+
.save(path)
52+
checkAnswer(
53+
spark.read.format("delta").load(path).selectExpr("s", "to_json(v)"),
54+
Seq(Row("foo", "99"), Row("foo", "100"), Row("foo", "101")))
55+
}
56+
}
57+
58+
test("write and read a variant nested in a struct") {
59+
withTempDir {
60+
dir =>
61+
val path = dir.getCanonicalPath
62+
spark
63+
.range(2)
64+
.selectExpr(
65+
"'foo' AS s",
66+
"named_struct('inner', parse_json(cast(id + 99 AS string))) AS v")
67+
.write
68+
.format("delta")
69+
.mode("overwrite")
70+
.save(path)
71+
checkAnswer(
72+
spark.read.format("delta").load(path).selectExpr("s", "to_json(v.inner)"),
73+
Seq(Row("foo", "99"), Row("foo", "100")))
74+
}
75+
}
76+
77+
test("update a table with a variant column") {
78+
withTempDir {
79+
dir =>
80+
val path = dir.getCanonicalPath
81+
spark
82+
.range(3)
83+
.selectExpr("cast(id AS int) AS id", "parse_json(cast(id AS string)) AS v")
84+
.write
85+
.format("delta")
86+
.mode("overwrite")
87+
.save(path)
88+
sql(s"UPDATE delta.`$path` SET v = parse_json('123') WHERE id = 1")
89+
checkAnswer(
90+
spark.read.format("delta").load(path).selectExpr("id", "to_json(v)"),
91+
Seq(Row(0, "0"), Row(1, "123"), Row(2, "2")))
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)