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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2991f50d9066 fix: Fix schema evolution decimal precision overflow 
(#20130)
2991f50d9066 is described below

commit 2991f50d9066051bf4230cd650b8caa2914f00ef
Author: Ashok Raminedi <[email protected]>
AuthorDate: Fri Oct 2 10:20:41 2026 -0400

    fix: Fix schema evolution decimal precision overflow (#20130)
    
    * Fix schema evolution decimal precision overflow
    
    * fix: handle decimal precision overflow in vectorized reads
    
    * Fix schema evolution overflow assertion
---
 .../client/utils/SparkInternalSchemaConverter.java |  32 +++-
 .../util/TestSparkInternalSchemaConverter.scala    |  42 ++++-
 .../TestVectorizedReadWithSchemaEvolution.scala    | 205 ++++++++++++++++-----
 3 files changed, 228 insertions(+), 51 deletions(-)

diff --git 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkInternalSchemaConverter.java
 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkInternalSchemaConverter.java
index f948b7ea5829..08feb7f38539 100644
--- 
a/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkInternalSchemaConverter.java
+++ 
b/hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/utils/SparkInternalSchemaConverter.java
@@ -28,6 +28,7 @@ import org.apache.hudi.common.util.collection.Pair;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import org.apache.spark.sql.execution.vectorized.WritableColumnVector;
+import org.apache.spark.sql.internal.SQLConf;
 import org.apache.spark.sql.types.ArrayType;
 import org.apache.spark.sql.types.ArrayType$;
 import org.apache.spark.sql.types.BinaryType;
@@ -325,6 +326,22 @@ public class SparkInternalSchemaConverter {
     }
   }
 
+  private static void putDecimalOrNull(
+      WritableColumnVector newV, int rowId, Decimal decimal, DecimalType 
decimalType) {
+    if (decimal.changePrecision(decimalType.precision(), decimalType.scale())) 
{
+      newV.putDecimal(rowId, decimal, decimalType.precision());
+    } else if (SQLConf.get().ansiEnabled()) {
+      throw new ArithmeticException(
+          String.format(
+              "%s cannot be represented as Decimal(%d, %d)",
+              decimal,
+              decimalType.precision(),
+              decimalType.scale()));
+    } else {
+      newV.putNull(rowId);
+    }
+  }
+
   /**
    * Convert Int/long type to other Type.
    * Now only support int/long -> long/float/double/string/Decimal
@@ -349,9 +366,9 @@ public class SparkInternalSchemaConverter {
         } else if (newType instanceof StringType) {
           newV.putByteArray(i, getUTF8Bytes((isInt ? oldV.getInt(i) : 
oldV.getLong(i)) + ""));
         } else if (newType instanceof DecimalType) {
+          DecimalType decimalType = (DecimalType) newType;
           Decimal oldDecimal = Decimal.apply(isInt ? oldV.getInt(i) : 
oldV.getLong(i));
-          oldDecimal.changePrecision(((DecimalType) newType).precision(), 
((DecimalType) newType).scale());
-          newV.putDecimal(i, oldDecimal, ((DecimalType) newType).precision());
+          putDecimalOrNull(newV, i, oldDecimal, decimalType);
         }
       }
       return true;
@@ -377,9 +394,9 @@ public class SparkInternalSchemaConverter {
         } else if (newType instanceof StringType) {
           newV.putByteArray(i, getUTF8Bytes(oldV.getFloat(i) + ""));
         } else if (newType instanceof DecimalType) {
+          DecimalType decimalType = (DecimalType) newType;
           Decimal oldDecimal = Decimal.apply(oldV.getFloat(i));
-          oldDecimal.changePrecision(((DecimalType) newType).precision(), 
((DecimalType) newType).scale());
-          newV.putDecimal(i, oldDecimal, ((DecimalType) newType).precision());
+          putDecimalOrNull(newV, i, oldDecimal, decimalType);
         }
       }
       return true;
@@ -401,9 +418,9 @@ public class SparkInternalSchemaConverter {
         }
         // double -> decimal/string
         if (newType instanceof DecimalType) {
+          DecimalType decimalType = (DecimalType) newType;
           Decimal oldDecimal = Decimal.apply(oldV.getDouble(i));
-          oldDecimal.changePrecision(((DecimalType) newType).precision(), 
((DecimalType) newType).scale());
-          newV.putDecimal(i, oldDecimal, ((DecimalType) newType).precision());
+          putDecimalOrNull(newV, i, oldDecimal, decimalType);
         } else if (newType instanceof StringType) {
           newV.putByteArray(i, getUTF8Bytes(oldV.getDouble(i) + ""));
         }
@@ -481,8 +498,7 @@ public class SparkInternalSchemaConverter {
           DecimalType decimalType = (DecimalType) newType;
           java.math.BigDecimal bigDecimal = new 
java.math.BigDecimal(oldV.getUTF8String(i).toString().trim());
           Decimal sparkDecimal = Decimal.apply(bigDecimal);
-          sparkDecimal.changePrecision(decimalType.precision(), 
decimalType.scale());
-          newV.putDecimal(i, sparkDecimal, decimalType.precision());
+          putDecimalOrNull(newV, i, sparkDecimal, decimalType);
         }
       }
       return true;
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/util/TestSparkInternalSchemaConverter.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/util/TestSparkInternalSchemaConverter.scala
index e2afc593d71f..0b55eb33f0cd 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/util/TestSparkInternalSchemaConverter.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/hudi/util/TestSparkInternalSchemaConverter.scala
@@ -20,12 +20,14 @@
 package org.apache.hudi.util
 
 import org.apache.hudi.SparkAdapterSupport
-import 
org.apache.hudi.client.utils.SparkInternalSchemaConverter.collectColNamesFromSparkStruct
+import 
org.apache.hudi.client.utils.SparkInternalSchemaConverter.{collectColNamesFromSparkStruct,
 convertColumnVectorType}
 import org.apache.hudi.common.schema.HoodieSchema
 import 
org.apache.hudi.common.schema.internal.convert.TestInternalSchemaConverter._
 import org.apache.hudi.testutils.HoodieSparkClientTestHarness
 
 import org.apache.spark.sql.avro.HoodieSparkSchemaConverters
+import org.apache.spark.sql.execution.vectorized.OnHeapColumnVector
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
 import org.junit.jupiter.api.Assertions.{assertEquals, assertTrue}
 import org.junit.jupiter.api.Test
@@ -36,6 +38,44 @@ class TestSparkInternalSchemaConverter extends 
HoodieSparkClientTestHarness with
     HoodieSparkSchemaConverters.toSqlType(schema)._1
   }
 
+  @Test
+  def testDecimalPrecisionOverflow(): Unit = {
+    val conf = SQLConf.get
+    val previousAnsi = conf.getConfString("spark.sql.ansi.enabled", null)
+    conf.setConfString("spark.sql.ansi.enabled", "false")
+
+    try {
+      val decimalType = DecimalType(4, 2)
+
+      Seq[(DataType, OnHeapColumnVector => Unit)](
+        IntegerType -> (_.putInt(0, 12345)),
+        LongType -> (_.putLong(0, 12345L)),
+        FloatType -> (_.putFloat(0, 12345.0f)),
+        DoubleType -> (_.putDouble(0, 12345.0)),
+        StringType -> (_.putByteArray(0, "12345".getBytes("UTF-8")))
+      ).foreach { case (sourceType, putValue) =>
+        val oldV = new OnHeapColumnVector(1, sourceType)
+        val newV = new OnHeapColumnVector(1, decimalType)
+
+        try {
+          putValue(oldV)
+
+          assertTrue(convertColumnVectorType(oldV, newV, 1))
+          assertTrue(newV.isNullAt(0), s"Expected NULL for $sourceType -> 
$decimalType overflow")
+        } finally {
+          oldV.close()
+          newV.close()
+        }
+      }
+    } finally {
+      if (previousAnsi == null) {
+        conf.unsetConf("spark.sql.ansi.enabled")
+      } else {
+        conf.setConfString("spark.sql.ansi.enabled", previousAnsi)
+      }
+    }
+  }
+
   @Test
   def testCollectColumnNames(): Unit = {
     val simpleSchema = StructType(Seq(
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestVectorizedReadWithSchemaEvolution.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestVectorizedReadWithSchemaEvolution.scala
index 5738621bea18..263627943c74 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestVectorizedReadWithSchemaEvolution.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/TestVectorizedReadWithSchemaEvolution.scala
@@ -17,53 +17,174 @@
 
 package org.apache.spark.sql.hudi.common
 
-import org.apache.hudi.HoodieSparkUtils
+import org.apache.spark.SparkException
 
 class TestVectorizedReadWithSchemaEvolution extends HoodieSparkSqlTestBase {
   Seq("cow", "mor").foreach { tableType =>
     test(s"Test vectorized read for $tableType table") {
-      if (HoodieSparkUtils.isSpark3) {
-        withSQLConf(
-          "hoodie.schema.on.read.enable" -> "true",
-          "spark.sql.parquet.enableVectorizedReader" -> "true",
-          "spark.sql.codegen.maxFields" -> "1",
-          "hoodie.parquet.small.file.limit" -> "0"
-        ) {
-          withTempDir { tmp =>
-            val tableName = generateTableName
-            val tablePath = s"${tmp.getCanonicalPath}/$tableName"
-            // create table
-            spark.sql(
-              s"""
-                 |create table $tableName (
-                 |  id int,
-                 |  name string,
-                 |  price double,
-                 |  ts long
-                 |) using hudi
-                 | partitioned by (ts)
-                 | location '$tablePath'
-                 | tblproperties (
-                 |  type = '$tableType',
-                 |  primaryKey = 'id',
-                 |  orderingFields = 'ts'
-                 | )
-         """.stripMargin)
-            // insert data to table
-            spark.sql(s"insert into $tableName values(1, 'a1', 10, 1000)")
-
-            // alter table change data type of column 'id'
-            spark.sql(s"alter table $tableName alter column price type string")
-
-            // insert new records
-            spark.sql(s"insert into $tableName values(2, 'a2', '20', 2000)")
-
-            checkAnswer(s"select id, name, price, ts from $tableName")(
-              Seq(1, "a1", "10.0", 1000),
-              Seq(2, "a2", "20", 2000)
-            )
-          }
+      withSQLConf(
+        "hoodie.schema.on.read.enable" -> "true",
+        "spark.sql.parquet.enableVectorizedReader" -> "true",
+        "spark.sql.codegen.maxFields" -> "1",
+        "hoodie.parquet.small.file.limit" -> "0"
+      ) {
+        withTempDir { tmp =>
+          val tableName = generateTableName
+          val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+          // create table
+          spark.sql(
+            s"""
+                |create table $tableName (
+                |  id int,
+                |  name string,
+                |  price double,
+                |  ts long
+                |) using hudi
+                | partitioned by (ts)
+                | location '$tablePath'
+                | tblproperties (
+                |  type = '$tableType',
+                |  primaryKey = 'id',
+                |  orderingFields = 'ts'
+                | )
+        """.stripMargin)
+          // insert data to table
+          spark.sql(s"insert into $tableName values(1, 'a1', 10, 1000)")
+
+          // alter table change data type of column 'id'
+          spark.sql(s"alter table $tableName alter column price type string")
+
+          // insert new records
+          spark.sql(s"insert into $tableName values(2, 'a2', '20', 2000)")
+
+          checkAnswer(s"select id, name, price, ts from $tableName")(
+            Seq(1, "a1", "10.0", 1000),
+            Seq(2, "a2", "20", 2000)
+          )
+        }
+      }
+    }
+  }
+
+  test(s"Test INT to DECIMAL schema evolution with precision overflow for COW 
table") {
+    withSQLConf(
+      "hoodie.schema.on.read.enable" -> "true",
+      "spark.sql.ansi.enabled" -> "false",
+      "spark.sql.parquet.enableVectorizedReader" -> "true",
+      "hoodie.parquet.small.file.limit" -> "0"
+    ) {
+      withTempDir { tmp =>
+        val tableName = generateTableName
+        val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+        spark.sql(
+          s"""
+              |create table $tableName (
+              |  id int,
+              |  price int,
+              |  ts long
+              |) using hudi
+              | location '$tablePath'
+              | tblproperties (
+              |  type = 'cow',
+              |  primaryKey = 'id',
+              |  orderingFields = 'ts'
+              | )
+              |""".stripMargin)
+
+        spark.sql(s"insert into $tableName values (1, 12345, 1000), (2, 12, 
1000)")
+
+        spark.sql(s"alter table $tableName alter column price type decimal(4, 
2)")
+
+        checkAnswer(s"select id, price from $tableName")(
+          Seq(1, null),
+          Seq(2, new java.math.BigDecimal("12.00"))
+        )
+      }
+    }
+  }
+
+  test(s"Test LONG to DECIMAL schema evolution with precision overflow for COW 
table") {
+    withSQLConf(
+      "hoodie.schema.on.read.enable" -> "true",
+      "spark.sql.ansi.enabled" -> "false",
+      "spark.sql.parquet.enableVectorizedReader" -> "true",
+      "hoodie.parquet.small.file.limit" -> "0"
+    ) {
+      withTempDir { tmp =>
+        val tableName = generateTableName
+        val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+        spark.sql(
+          s"""
+            |create table $tableName (
+            |  id int,
+            |  price bigint,
+            |  ts long
+            |) using hudi
+            | location '$tablePath'
+            | tblproperties (
+            |  type = 'cow',
+            |  primaryKey = 'id',
+            |  orderingFields = 'ts'
+            | )
+            |""".stripMargin)
+
+        spark.sql(s"insert into $tableName values (1, 9223372036854775807, 
1000)")
+
+        spark.sql(s"alter table $tableName alter column price type decimal(20, 
2)")
+
+        checkAnswer(s"select id, price from $tableName")(
+          Seq(1, null)
+        )
+      }
+    }
+  }
+
+  test(s"Test INT to DECIMAL schema evolution with precision overflow in ANSI 
mode for COW table") {
+    withSQLConf(
+      "hoodie.schema.on.read.enable" -> "true",
+      "spark.sql.ansi.enabled" -> "true",
+      "spark.sql.parquet.enableVectorizedReader" -> "true",
+      "hoodie.parquet.small.file.limit" -> "0"
+    ) {
+      withTempDir { tmp =>
+        val tableName = generateTableName
+        val tablePath = s"${tmp.getCanonicalPath}/$tableName"
+
+        spark.sql(
+          s"""
+            |create table $tableName (
+            |  id int,
+            |  price int,
+            |  ts long
+            |) using hudi
+            | location '$tablePath'
+            | tblproperties (
+            |  type = 'cow',
+            |  primaryKey = 'id',
+            |  orderingFields = 'ts'
+            | )
+            |""".stripMargin)
+
+        spark.sql(s"insert into $tableName values (1, 12345, 1000)")
+
+        spark.sql(s"alter table $tableName alter column price type decimal(4, 
2)")
+
+        val exception = intercept[SparkException] {
+          spark.sql(s"select id, price from $tableName").collect()
         }
+
+        val messages = Iterator
+          .iterate[Throwable](exception)(_.getCause)
+          .takeWhile(_ != null)
+          .flatMap(e => Option(e.getMessage))
+          .mkString("\n")
+
+        assert(
+          messages.contains("12345 cannot be represented as Decimal(4, 2)"),
+          s"Unexpected Spark error chain:\n$messages"
+        )
       }
     }
   }

Reply via email to