Skip to content

Commit ed2f775

Browse files
Ma77Ballkunwp1
andauthored
fix: preserve ANY type through schema round-trip (#4995)
### What changes were proposed in this PR? ArrowUtils.fromTexeraSchema now tags ANY attributes with texera_type=ANY metadata on the Arrow field, and toTexeraSchema reads that tag back. This mirrors the existing LARGE_BINARY mechanism. Without it, ANY round-trips silently became STRING because both types share the same Arrow representation (Utf8). ### Any related issues, documentation, or discussions? Closes: #4762 ### How was this PR tested? Updated ArrowUtilsSpec (in common/workflow-core): replaced the test that pinned the bug ("lose the ANY distinction") with one that asserts ANY is preserved through a round-trip, and added a test that the texera_type=ANY metadata is attached only to ANY fields. Ran both WorkflowCore (27/27) and WorkflowOperator (14/14) ArrowUtilsSpec suites — all pass. ### Was this PR authored or co-authored using generative AI tooling? Co-Authored with Claude Opus 4.7 in compliance with ASF --------- Co-authored-by: Kunwoo (Chris) <143021053+kunwp1@users.noreply.github.com>
1 parent 6b93b12 commit ed2f775

2 files changed

Lines changed: 36 additions & 13 deletions

File tree

common/workflow-core/src/main/scala/org/apache/texera/amber/util/ArrowUtils.scala

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import scala.jdk.CollectionConverters.CollectionHasAsScala
4545
import scala.language.implicitConversions
4646

4747
object ArrowUtils extends LazyLogging {
48+
private val TexeraTypeMetadataKey = "texera_type"
4849

4950
// Create a single allocator for the entire utility
5051
private val allocator: BufferAllocator = new RootAllocator()
@@ -94,19 +95,23 @@ object ArrowUtils extends LazyLogging {
9495

9596
/**
9697
* Converts an Arrow Schema into Texera Schema.
97-
* Checks field metadata to detect LARGE_BINARY types.
98+
* Checks field metadata to recover types that share an Arrow representation
99+
* (LARGE_BINARY and ANY both ride on Utf8).
98100
*
99101
* @param arrowSchema The Arrow Schema to be converted.
100102
* @return A Texera Schema.
101103
*/
102104
def toTexeraSchema(arrowSchema: org.apache.arrow.vector.types.pojo.Schema): Schema =
103105
Schema(
104106
arrowSchema.getFields.asScala.map { field =>
105-
val isLargeBinary = Option(field.getMetadata)
106-
.exists(m => m.containsKey("texera_type") && m.get("texera_type") == "LARGE_BINARY")
107+
val taggedType = Option(field.getMetadata)
108+
.flatMap(m => Option(m.get(TexeraTypeMetadataKey)))
109+
.collect {
110+
case "LARGE_BINARY" => AttributeType.LARGE_BINARY
111+
case "ANY" => AttributeType.ANY
112+
}
107113

108-
val attributeType =
109-
if (isLargeBinary) AttributeType.LARGE_BINARY else toAttributeType(field.getType)
114+
val attributeType = taggedType.getOrElse(toAttributeType(field.getType))
110115
new Attribute(field.getName, attributeType)
111116
}.toList
112117
)
@@ -232,16 +237,22 @@ object ArrowUtils extends LazyLogging {
232237

233238
/**
234239
* Converts an Amber schema into Arrow schema.
235-
* Stores AttributeType in field metadata to preserve LARGE_BINARY type information.
240+
* Stores AttributeType in field metadata to preserve LARGE_BINARY and ANY,
241+
* which both collapse onto Utf8 in Arrow.
236242
*
237243
* @param schema The Texera Schema.
238244
* @return An Arrow Schema.
239245
*/
240246
def fromTexeraSchema(schema: Schema): org.apache.arrow.vector.types.pojo.Schema = {
241247
val arrowFields = schema.getAttributes.map { attribute =>
242-
val metadata = if (attribute.getType == AttributeType.LARGE_BINARY) {
248+
val metadataTag = attribute.getType match {
249+
case AttributeType.LARGE_BINARY => "LARGE_BINARY"
250+
case AttributeType.ANY => "ANY"
251+
case _ => null
252+
}
253+
val metadata = if (metadataTag != null) {
243254
val map = new util.HashMap[String, String]()
244-
map.put("texera_type", "LARGE_BINARY")
255+
map.put(TexeraTypeMetadataKey, metadataTag)
245256
map
246257
} else null
247258

common/workflow-core/src/test/scala/org/apache/texera/amber/util/ArrowUtilsSpec.scala

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -258,14 +258,26 @@ class ArrowUtilsSpec extends AnyFlatSpec with Matchers {
258258
)
259259
}
260260

261-
it should "lose the ANY distinction (round-trips as STRING)" in {
262-
// Pin: ANY fromAttributeType produces Utf8 with no metadata. toAttributeType
263-
// then can only see Utf8, so the recovered type is STRING. Documenting this
264-
// information loss so a future fix that round-trips ANY can break the spec.
261+
it should "preserve ANY through the metadata-based path" in {
265262
val original = Schema(List(new Attribute("v", AttributeType.ANY)))
266263
val recovered = ArrowUtils.toTexeraSchema(ArrowUtils.fromTexeraSchema(original))
267264
recovered.getAttributes.toList.map(a => (a.getName, a.getType)) shouldBe List(
268-
("v", AttributeType.STRING)
265+
("v", AttributeType.ANY)
269266
)
270267
}
268+
269+
it should "attach texera_type=ANY metadata to ANY fields and only those" in {
270+
val schema = Schema(
271+
List(
272+
new Attribute("v", AttributeType.ANY),
273+
new Attribute("name", AttributeType.STRING)
274+
)
275+
)
276+
val arrow = ArrowUtils.fromTexeraSchema(schema)
277+
val fields = arrow.getFields.asScala.toList
278+
val any = fields.find(_.getName == "v").get
279+
val name = fields.find(_.getName == "name").get
280+
any.getMetadata.get("texera_type") shouldBe "ANY"
281+
Option(name.getMetadata).map(_.containsKey("texera_type")).getOrElse(false) shouldBe false
282+
}
271283
}

0 commit comments

Comments
 (0)