This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 70d98e5c79 [spark] Reject variant extraction paths containing the 
metadata delimiter (#9569)
70d98e5c79 is described below

commit 70d98e5c79e76e28f6d165168742b92e1dfa2828
Author: jackylee <[email protected]>
AuthorDate: Fri Sep 4 15:24:44 2026 +0800

    [spark] Reject variant extraction paths containing the metadata delimiter 
(#9569)
---
 .../paimon/data/variant/VariantMetadataUtils.java  |  7 ++++
 .../data/variant/VariantMetadataUtilsTest.java     |  8 ++++
 .../paimon/spark/read/VariantPushDownUtils.scala   | 10 ++++-
 .../spark/read/VariantPushDownUtilsTest.scala      | 45 ++++++++++++++++++++++
 4 files changed, 69 insertions(+), 1 deletion(-)

diff --git 
a/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantMetadataUtils.java
 
b/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantMetadataUtils.java
index a0a6208035..34bfc3e7a3 100644
--- 
a/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantMetadataUtils.java
+++ 
b/paimon-common/src/main/java/org/apache/paimon/data/variant/VariantMetadataUtils.java
@@ -27,6 +27,8 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static org.apache.paimon.utils.Preconditions.checkArgument;
+
 /**
  * Utils for marking and identifying variant-originated RowType. Uses 
description field in DataField
  * to encode variant metadata.
@@ -42,6 +44,11 @@ public class VariantMetadataUtils {
 
     /** Build variant metadata description string. */
     public static String buildVariantMetadata(String path, boolean 
failOnError, String timeZoneId) {
+        checkArgument(
+                !path.contains(DELIMITER),
+                "Variant extraction path must not contain '%s': %s",
+                DELIMITER,
+                path);
         return METADATA_KEY + path + DELIMITER + failOnError + DELIMITER + 
timeZoneId;
     }
 
diff --git 
a/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantMetadataUtilsTest.java
 
b/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantMetadataUtilsTest.java
index 815fa0ccaa..cea9c27be0 100644
--- 
a/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantMetadataUtilsTest.java
+++ 
b/paimon-common/src/test/java/org/apache/paimon/data/variant/VariantMetadataUtilsTest.java
@@ -28,6 +28,7 @@ import java.time.ZoneId;
 
 import static 
org.apache.paimon.data.variant.VariantMetadataUtils.VariantRowTypeBuilder;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 /** Tests for {@link VariantMetadataUtils}. */
 public class VariantMetadataUtilsTest {
@@ -38,6 +39,13 @@ public class VariantMetadataUtilsTest {
         assertThat(metadata).isEqualTo("__VARIANT_METADATA$.a.b;true;UTC");
     }
 
+    @Test
+    public void testBuildVariantMetadataRejectsPathWithDelimiter() {
+        assertThatThrownBy(() -> 
VariantMetadataUtils.buildVariantMetadata("$.a;b", true, "UTC"))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("must not contain ';'");
+    }
+
     @Test
     public void testIsVariantRow() {
         // Create a row type with variant metadata
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VariantPushDownUtils.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VariantPushDownUtils.scala
index 1ac017ac66..17b68a011e 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VariantPushDownUtils.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/read/VariantPushDownUtils.scala
@@ -42,7 +42,7 @@ object VariantPushDownUtils {
     while (i < extractions.length) {
       val (path, info, isVariantTarget) = extractions(i)
       val canThrow = info.failOnError && 
!info.paimonType.isInstanceOf[VarCharType]
-      if (path.isEmpty || isVariantTarget || canThrow) {
+      if (path.isEmpty || isVariantTarget || canThrow || 
!canEncodePath(info.path)) {
         if (path.nonEmpty) {
           rejected += path
         }
@@ -68,6 +68,14 @@ object VariantPushDownUtils {
     (out.toMap, accepted)
   }
 
+  /**
+   * The extraction path is encoded into the projected field's description, 
delimited by
+   * [[VariantMetadataUtils.DELIMITER]] and split back on read with no 
escaping. A path carrying the
+   * delimiter would decode into a different path, so it cannot be pushed down.
+   */
+  private def canEncodePath(path: String): Boolean =
+    !path.contains(VariantMetadataUtils.DELIMITER)
+
   /**
    * Replace each variant field at a path in `accepted` with a Paimon variant 
`RowType`; recurse
    * into nested structs.
diff --git 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/read/VariantPushDownUtilsTest.scala
 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/read/VariantPushDownUtilsTest.scala
new file mode 100644
index 0000000000..27ce947662
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/read/VariantPushDownUtilsTest.scala
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.spark.read
+
+import org.apache.paimon.types.DataTypes
+
+import org.scalatest.funsuite.AnyFunSuite
+
+/** Tests for {@link VariantPushDownUtils}. */
+class VariantPushDownUtilsTest extends AnyFunSuite {
+
+  private def extraction(path: String) =
+    (Seq("v"), VariantExtractionInfo(DataTypes.STRING(), path, false, "UTC"), 
false)
+
+  test("accept an extraction path that can be encoded") {
+    val (byPath, accepted) = 
VariantPushDownUtils.acceptByPath(IndexedSeq(extraction("$.a.b")))
+    assert(accepted === Array(true))
+    assert(byPath.keySet === Set(Seq("v")))
+  }
+
+  test("reject an extraction path containing the metadata delimiter") {
+    Seq("$.a;b", "$[\"a;b\"]").foreach {
+      path =>
+        val (byPath, accepted) = 
VariantPushDownUtils.acceptByPath(IndexedSeq(extraction(path)))
+        assert(accepted === Array(false))
+        assert(byPath.isEmpty)
+    }
+  }
+}

Reply via email to