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 365d0e41d5f7 fix(spark): throw proper ParseExceptions in the six 
extended SQL AST... (#19460)
365d0e41d5f7 is described below

commit 365d0e41d5f7cf7fecc43e7837cc45789d1dec4f
Author: voonhous <[email protected]>
AuthorDate: Mon Aug 3 14:23:24 2026 +0800

    fix(spark): throw proper ParseExceptions in the six extended SQL AST... 
(#19460)
    
    * fix(spark): throw proper ParseExceptions in the six extended SQL AST 
builders
    
    On Spark 3.4+ the (String, ParserRuleContext) ParseException constructor
    treats the string as an error class, so the builders' 28 raw throw sites
    surfaced at runtime as SparkException [INTERNAL_ERROR] (or a bare
    AssertionError for messages containing dots), and the three
    construct-then-setStackTrace sites discarded the original cause. Route
    all sites through a per-version parseException helper (message-based
    constructor on 3.3-3.5, _LEGACY_ERROR_TEMP_0035 on 4.x where the message
    constructor is private), fix the unbraced ${nonRef.describe}
    interpolation, and tighten the invalid-partition-transform test to
    intercept[ParseException].
    
    Fixes #19450
    
    * test(spark): harden extended-parser negative coverage and polish the 4.x 
helper
    
    Add a shared interceptParse helper that asserts a clean ParseException plus
    a message substring; the previous checkExceptionContain assertions catch any
    Throwable, and the pre-fix [INTERNAL_ERROR] text embeds the original 
message,
    so they passed while every site was broken. Convert the negative parser
    assertions in TestBlobDataType and TestCreateTable, add four cases for the
    previously untested visitor arms (typed literals, invalid INTERVAL, the
    out-of-range AssertionError mode, mixed interval fields), pin the
    ${nonRef.describe} rendering, strip the trailing period in the 4.x helper
    to avoid double periods, document the cross-profile prefix and the
    _LEGACY_ERROR_TEMP_0035 coupling, and mark the grammar-unreachable interval
    arm for the next pruning pass.
    
    * test(spark): cover remaining reachable builder throw sites and pin review 
asserts
    
    Address review comments: pin the interpolation fix by asserting the
    absence of ".describe" in the column-reference message, and assert the
    restored setStackTrace by checking for an IntervalUtils frame on the
    invalid-INTERVAL case. Address the codecov patch-coverage gap with a new
    negative test covering seven more converted throw sites (unsupported
    typed literal, hex IllegalArgumentException fallback, both
    single-from-to-unit arms, non-numeric unit value, non-string from-to
    value, unsupported from-to pair); interceptParse now returns the
    ParseException for follow-on assertions. The remaining uncovered sites
    re-wrap Spark-utility exceptions whose types changed across 3.3-4.2
    (e.g. over-precision decimals throw SparkArithmeticException on 4.x,
    bypassing the AnalysisException catch), are grammar-unreachable, or need
    ANTLR error-recovery trees.
---
 .../hudi/common/ExtendedParserTestHelpers.scala    | 15 +++++
 .../spark/sql/hudi/ddl/TestCreateTable.scala       |  8 +--
 .../sql/hudi/dml/schema/TestBlobDataType.scala     | 72 +++++++++++++++++-----
 .../HoodieSpark3_3ExtendedSqlAstBuilder.scala      | 69 ++++++++++++---------
 .../HoodieSpark3_4ExtendedSqlAstBuilder.scala      | 71 ++++++++++++---------
 .../HoodieSpark3_5ExtendedSqlAstBuilder.scala      | 71 ++++++++++++---------
 .../HoodieSpark4_0ExtendedSqlAstBuilder.scala      | 69 ++++++++++++---------
 .../HoodieSpark4_1ExtendedSqlAstBuilder.scala      | 69 ++++++++++++---------
 .../HoodieSpark4_2ExtendedSqlAstBuilder.scala      | 69 ++++++++++++---------
 9 files changed, 322 insertions(+), 191 deletions(-)

diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/ExtendedParserTestHelpers.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/ExtendedParserTestHelpers.scala
index d2f721440228..dfc1dd7c7a73 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/ExtendedParserTestHelpers.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/common/ExtendedParserTestHelpers.scala
@@ -18,6 +18,7 @@
 package org.apache.spark.sql.hudi.common
 
 import org.apache.spark.sql.SparkSession
+import org.apache.spark.sql.catalyst.parser.ParseException
 import org.apache.spark.sql.catalyst.plans.logical.CreateTable
 import org.apache.spark.sql.connector.expressions.{FieldReference, 
LiteralValue, Transform}
 import org.scalatest.Assertions
@@ -36,6 +37,20 @@ trait ExtendedParserTestHelpers extends Assertions {
   protected def parseCreateTable(sql: String): CreateTable =
     spark.sessionState.sqlParser.parsePlan(sql).asInstanceOf[CreateTable]
 
+  /**
+   * Asserts that `sql` fails with a [[ParseException]] (not merely some 
Exception) whose message
+   * contains `expected`, and returns the exception so callers can assert 
further properties (e.g.
+   * stack-trace frames). Matching stays on a substring so the same assertion 
holds on Spark 4.x,
+   * where the builders wrap the message in an "Operation not allowed: " 
prefix.
+   */
+  protected def interceptParse(sql: String)(expected: String): ParseException 
= {
+    val e = intercept[ParseException] {
+      spark.sql(sql)
+    }
+    assert(e.getMessage.contains(expected), s"actual: ${e.getMessage}")
+    e
+  }
+
   protected def transformByName(plan: CreateTable, name: String): Transform =
     plan.partitioning.find(_.name == name)
       .getOrElse(fail(s"No partition transform named $name in 
${plan.partitioning.mkString(", ")}"))
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
index cf7135b07cba..7c87f5c034a6 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/ddl/TestCreateTable.scala
@@ -2286,7 +2286,7 @@ class TestCreateTable extends HoodieSparkSqlTestBase with 
ExtendedParserTestHelp
   test("test create table with VECTOR without dimension fails") {
     withTempDir { tmp =>
       val tableName = generateTableName
-      checkExceptionContain(
+      interceptParse(
         s"""
            |CREATE TABLE $tableName (
            |  id BIGINT,
@@ -2304,7 +2304,7 @@ class TestCreateTable extends HoodieSparkSqlTestBase with 
ExtendedParserTestHelp
     withTempDir { tmp =>
       val tableName = generateTableName
       // Unsupported element type
-      checkExceptionContain(
+      interceptParse(
         s"""
            |CREATE TABLE $tableName (
            |  id BIGINT,
@@ -2403,14 +2403,14 @@ class TestCreateTable extends HoodieSparkSqlTestBase 
with ExtendedParserTestHelp
     assertEquals(Some("/tmp/vec_path_tbl"), pathPlan.tableSpec.location)
 
     // A 'path' option colliding with LOCATION is rejected by the option 
cleaner.
-    checkExceptionContain(
+    interceptParse(
       "CREATE TABLE vec_dup_path_tbl (id BIGINT, embedding VECTOR(4)) USING 
hudi " +
         "OPTIONS ('path' = '/tmp/a') LOCATION '/tmp/b'")(
       "Duplicated table paths")
 
     // Each reserved table property (provider, location, owner) is rejected by 
the property cleaner.
     Seq("provider", "location", "owner").foreach { reserved =>
-      checkExceptionContain(
+      interceptParse(
         s"CREATE TABLE vec_reserved_$reserved (id BIGINT, embedding VECTOR(4)) 
USING hudi " +
           s"TBLPROPERTIES ('$reserved' = 'x')")(
         "reserved table property")
diff --git 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala
 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala
index 42d7bfcfba68..368506bfd076 100644
--- 
a/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala
+++ 
b/hudi-spark-datasource/hudi-spark/src/test/scala/org/apache/spark/sql/hudi/dml/schema/TestBlobDataType.scala
@@ -382,10 +382,10 @@ class TestBlobDataType extends HoodieSparkSqlTestBase 
with ExtendedParserTestHel
     // Endpoints where the end field does not follow the start are rejected by 
both interval
     // data-type visitors. The grammar only allows YEAR/MONTH -> MONTH and 
DAY/HOUR/MINUTE/SECOND
     // -> HOUR/MINUTE/SECOND, so these stay grammatical yet still hit the 
builder's end <= start guard.
-    checkExceptionContain(
+    interceptParse(
       "CREATE TABLE blob_bad_ym (id BIGINT, bad INTERVAL MONTH TO MONTH, data 
BLOB) USING hudi")(
       "are not supported")
-    checkExceptionContain(
+    interceptParse(
       "CREATE TABLE blob_bad_dt (id BIGINT, bad INTERVAL SECOND TO HOUR, data 
BLOB) USING hudi")(
       "are not supported")
   }
@@ -502,23 +502,61 @@ class TestBlobDataType extends HoodieSparkSqlTestBase 
with ExtendedParserTestHel
   }
 
   test("Test parse CREATE TABLE with BLOB column and invalid partition 
transforms") {
-    // Non-numeric number of buckets. The builders' raw `new 
ParseException(message, ctx)` sites
-    // surface on Spark 3.4+ as SparkException [INTERNAL_ERROR] wrapping the 
message text
-    // (#19450), so this case asserts the message via a plain intercept.
-    // TODO(#19450): tighten to intercept[ParseException] once the builders 
throw it cleanly.
-    val e = intercept[Exception] {
-      spark.sql("CREATE TABLE blob_e1 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (bucket('x', id))")
-    }
-    assert(e.getMessage.contains("Invalid number of buckets"))
-    // A non-column-reference where a column is required.
-    checkExceptionContain(
-      "CREATE TABLE blob_e2 (id BIGINT, data BLOB) USING hudi PARTITIONED BY 
(bucket(4, 5))")(
-      "Expected a column reference")
+    // Each case pins a distinct visitor arm of the extended AST builders; all 
must surface as a
+    // clean ParseException on every Spark profile (#19450). Assertions stay 
substring-based
+    // because the Spark 4.x builders add an "Operation not allowed: " prefix.
+    // Non-numeric number of buckets.
+    interceptParse("CREATE TABLE blob_e1 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (bucket('x', id))")(
+      "Invalid number of buckets")
+    // A non-column-reference where a column is required. The buggy 
interpolation rendered the
+    // literal text "5.describe" (a superstring of the expected message), so 
pin its absence too.
+    val e2 = interceptParse("CREATE TABLE blob_e2 (id BIGINT, data BLOB) USING 
hudi PARTITIONED BY (bucket(4, 5))")(
+      "Expected a column reference for transform bucket: 5")
+    assert(!e2.getMessage.contains(".describe"))
     // A single-field transform given more than one argument.
-    checkExceptionContain(
-      "CREATE TABLE blob_e3 (id BIGINT, ts DATE, data BLOB) USING hudi " +
-        "PARTITIONED BY (years(id, ts))")(
+    interceptParse("CREATE TABLE blob_e3 (id BIGINT, ts DATE, data BLOB) USING 
hudi PARTITIONED BY (years(id, ts))")(
       "Too many arguments")
+    // Typed literal that fails to parse (visitTypeConstructor arm).
+    interceptParse("CREATE TABLE blob_e4 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(DATE 'nope', id))")(
+      "Cannot parse the DATE value: nope")
+    // Invalid INTERVAL literal: the builders copy the triggering exception's 
stack trace onto the
+    // ParseException (the construct-then-setStackTrace arm), so the thrower 
must be visible.
+    val e5 = interceptParse("CREATE TABLE blob_e5 (id BIGINT, data BLOB) USING 
hudi PARTITIONED BY (myfunc(INTERVAL 'x', id))")(
+      "Cannot parse the INTERVAL value: x")
+    assert(e5.getStackTrace.exists(_.getClassName.contains("IntervalUtils")))
+    // Out-of-range fractional literal; pre-fix the message's interior dots 
broke Spark 3.4+
+    // error-class lookup and surfaced as a bare AssertionError.
+    interceptParse("CREATE TABLE blob_e6 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (bucket(1e40F, id))")(
+      "does not fit in range")
+    // Mixed year-month and day-time interval fields.
+    interceptParse("CREATE TABLE blob_e7 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(INTERVAL '1 year 2 hours', id))")(
+      "Cannot mix year-month and day-time fields")
+  }
+
+  test("Test parse CREATE TABLE with BLOB column and invalid literal transform 
arguments") {
+    // Remaining error arms of the literal and interval visitors, one SQL per 
throw site; all must
+    // surface as a clean ParseException on every Spark profile (#19450).
+    // A typed literal whose type keyword has no visitor arm.
+    interceptParse("CREATE TABLE blob_e8 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(FOO 'bar', id))")(
+      "Literals of type 'FOO' are currently not supported")
+    // A hex literal with a non-hex character (the IllegalArgumentException 
fallback arm).
+    interceptParse("CREATE TABLE blob_e9 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(X'zz', id))")(
+      "hexBinary")
+    // Multi-unit interval combined with a from-to unit.
+    interceptParse("CREATE TABLE blob_e10 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(INTERVAL 1 DAY 2 HOUR TO MINUTE, id))")(
+      "Can only have a single from-to unit in the interval literal syntax")
+    // From-to unit combined with a trailing multi-unit interval (the 
error-recovery arm).
+    interceptParse("CREATE TABLE blob_e11 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(INTERVAL '1' DAY TO HOUR '2' MINUTE, id))")(
+      "Can only have a single from-to unit in the interval literal syntax")
+    // A non-numeric value in a unit-value pair.
+    interceptParse("CREATE TABLE blob_e12 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(INTERVAL 'x' DAY, id))")(
+      "Can only use numbers in the interval value part")
+    // A from-to interval whose value is not a string literal.
+    interceptParse("CREATE TABLE blob_e13 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(INTERVAL 1 DAY TO HOUR, id))")(
+      "The value of from-to unit must be a string")
+    // A from-to unit pair outside the supported YEAR TO MONTH / DAY TO SECOND 
family.
+    interceptParse("CREATE TABLE blob_e14 (id BIGINT, data BLOB) USING hudi 
PARTITIONED BY (myfunc(INTERVAL '1' MONTH TO HOUR, id))")(
+      "Intervals FROM month TO hour are not supported")
   }
 
   test("Test parse CREATE TABLE with BLOB column and file-format / row-format 
clauses") {
diff --git 
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_3ExtendedSqlAstBuilder.scala
 
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_3ExtendedSqlAstBuilder.scala
index 06a51d02bf24..5c04157c4e35 100644
--- 
a/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_3ExtendedSqlAstBuilder.scala
+++ 
b/hudi-spark-datasource/hudi-spark3.3.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_3ExtendedSqlAstBuilder.scala
@@ -57,6 +57,17 @@ import scala.collection.JavaConverters._
 class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, delegate: 
ParserInterface)
   extends HoodieSqlBaseBaseVisitor[AnyRef] with Logging {
 
+  /**
+   * Returns a [[ParseException]] carrying `message` as plain human-readable 
text, positioned at
+   * `ctx`. Spark 3.3's (String, ParserRuleContext) constructor takes the 
human message directly,
+   * unlike Spark 3.4+ where that constructor treats the string as an error 
class (#19450).
+   * The Spark 4.x builders render the same messages behind an "Operation not 
allowed: " prefix,
+   * so cross-profile test assertions must stay substring-based.
+   */
+  private def parseException(message: String, ctx: ParserRuleContext): 
ParseException = {
+    new ParseException(message, ctx)
+  }
+
   protected def typedVisit[T](ctx: ParseTree): T = {
     ctx.accept(this).asInstanceOf[T]
   }
@@ -130,7 +141,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
 
     def toLiteral[T](f: UTF8String => Option[T], t: DataType): Literal = {
       f(UTF8String.fromString(value)).map(Literal(_, t)).getOrElse {
-        throw new ParseException(s"Cannot parse the $valueType value: $value", 
ctx)
+        throw parseException(s"Cannot parse the $valueType value: $value", ctx)
       }
     }
 
@@ -179,7 +190,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.stringToInterval(UTF8String.fromString(value))
           } catch {
             case e: IllegalArgumentException =>
-              val ex = new ParseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
+              val ex = parseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
               ex.setStackTrace(e.getStackTrace)
               throw ex
           }
@@ -196,12 +207,12 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           val padding = if (value.length % 2 != 0) "0" else ""
           Literal(DatatypeConverter.parseHexBinary(padding + value))
         case other =>
-          throw new ParseException(s"Literals of type '$other' are currently 
not supported.", ctx)
+          throw parseException(s"Literals of type '$other' are currently not 
supported.", ctx)
       }
     } catch {
       case e: IllegalArgumentException =>
         val message = Option(e.getMessage).getOrElse(s"Exception parsing 
$valueType")
-        throw new ParseException(message, ctx)
+        throw parseException(message, ctx)
     }
   }
 
@@ -274,13 +285,13 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     try {
       val rawBigDecimal = BigDecimal(rawStrippedQualifier)
       if (rawBigDecimal < minValue || rawBigDecimal > maxValue) {
-        throw new ParseException(s"Numeric literal $rawStrippedQualifier does 
not " +
+        throw parseException(s"Numeric literal $rawStrippedQualifier does not 
" +
           s"fit in range [$minValue, $maxValue] for type $typeName", ctx)
       }
       Literal(converter(rawStrippedQualifier))
     } catch {
       case e: NumberFormatException =>
-        throw new ParseException(e.getMessage, ctx)
+        throw parseException(e.getMessage, ctx)
     }
   }
 
@@ -338,7 +349,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       Literal(BigDecimal(raw).underlying())
     } catch {
       case e: AnalysisException =>
-        throw new ParseException(e.message, ctx)
+        throw parseException(e.message, ctx)
     }
   }
 
@@ -389,7 +400,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (yearMonthFields.nonEmpty) {
       if (dayTimeFields.nonEmpty) {
         val literalStr = source(ctx)
-        throw new ParseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
+        throw parseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
       }
       Literal(
         calendarInterval.months,
@@ -446,18 +457,20 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (ctx.errorCapturingMultiUnitsInterval != null) {
       val innerCtx = ctx.errorCapturingMultiUnitsInterval
       if (innerCtx.unitToUnitInterval != null) {
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
       }
       visitMultiUnitsInterval(innerCtx.multiUnitsInterval)
     } else if (ctx.errorCapturingUnitToUnitInterval != null) {
       val innerCtx = ctx.errorCapturingUnitToUnitInterval
       if (innerCtx.error1 != null || innerCtx.error2 != null) {
         val errorCtx = if (innerCtx.error1 != null) innerCtx.error1 else 
innerCtx.error2
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
       }
       visitUnitToUnitInterval(innerCtx.body)
     } else {
-      throw new ParseException("at least one time unit should be given for 
interval literal", ctx)
+      // Unreachable through Hudi's pruned grammar: a bare INTERVAL keyword 
binds to
+      // transformArgument's qualifiedName alternative first. Kept for parity 
with Spark.
+      throw parseException("at least one time unit should be given for 
interval literal", ctx)
     }
   }
 
@@ -479,7 +492,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             // units and become valid ones, e.g. '1 day 2 hour'.
             // Ideally, we only ensure the value parts don't contain any units 
here.
             if (value.exists(Character.isLetter)) {
-              throw new ParseException("Can only use numbers in the interval 
value part for" +
+              throw parseException("Can only use numbers in the interval value 
part for" +
                 s" multiple unit value pairs interval form, but got invalid 
value: $value", ctx)
             }
             if (values(i).MINUS() == null) {
@@ -498,7 +511,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         IntervalUtils.stringToInterval(UTF8String.concat(kvs: _*))
       } catch {
         case i: IllegalArgumentException =>
-          val e = new ParseException(i.getMessage, ctx)
+          val e = parseException(i.getMessage, ctx)
           e.setStackTrace(i.getStackTrace)
           throw e
       }
@@ -520,7 +533,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           }
         }
       }.getOrElse {
-        throw new ParseException("The value of from-to unit must be a string", 
ctx.intervalValue)
+        throw parseException("The value of from-to unit must be a string", 
ctx.intervalValue)
       }
       try {
         val from = ctx.from.getText.toLowerCase(Locale.ROOT)
@@ -533,12 +546,12 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.fromDayTimeString(value,
               DayTimeIntervalType.stringToField(from), 
DayTimeIntervalType.stringToField(to))
           case _ =>
-            throw new ParseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
+            throw parseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
         }
       } catch {
         // Handle Exceptions thrown by CalendarInterval
         case e: IllegalArgumentException =>
-          val pe = new ParseException(e.getMessage, ctx)
+          val pe = parseException(e.getMessage, ctx)
           pe.setStackTrace(e.getStackTrace)
           throw pe
       }
@@ -579,7 +592,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           
HoodieSchema.parseTypeDescriptor(ctx.getText).asInstanceOf[HoodieSchema.Vector]
         } catch {
           case e: IllegalArgumentException =>
-            throw new ParseException(s"Invalid VECTOR type: ${e.getMessage}", 
ctx)
+            throw parseException(s"Invalid VECTOR type: ${e.getMessage}", ctx)
         }
         val sparkElemType = vectorSchema.getVectorElementType match {
           case HoodieSchema.Vector.VectorElementType.FLOAT => FloatType
@@ -596,7 +609,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       case ("interval", Nil) => CalendarIntervalType
       case (dt, params) =>
         val dtStr = if (params.nonEmpty) s"$dt(${params.mkString(",")})" else 
dt
-        throw new ParseException(s"DataType $dtStr is not supported.", ctx)
+        throw parseException(s"DataType $dtStr is not supported.", ctx)
     }
   }
 
@@ -607,7 +620,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = YearMonthIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       YearMonthIntervalType(start, end)
     } else {
@@ -622,7 +635,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = DayTimeIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       DayTimeIntervalType(start, end)
     } else {
@@ -890,7 +903,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         case ref: FieldReference =>
           ref
         case nonRef =>
-          throw new ParseException(s"Expected a column reference for transform 
$name: $nonRef.describe", ctx)
+          throw parseException(s"Expected a column reference for transform 
$name: ${nonRef.describe}", ctx)
       }
     }
 
@@ -899,7 +912,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
                                  arguments: Seq[V2Expression]): FieldReference 
= {
       lazy val name: String = ctx.identifier.getText
       if (arguments.size > 1) {
-        throw new ParseException(s"Too many arguments for transform $name", 
ctx)
+        throw parseException(s"Too many arguments for transform $name", ctx)
       } else {
         getFieldReference(ctx, arguments.head)
       }
@@ -922,7 +935,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
               case LiteralValue(longValue, LongType) =>
                 longValue.asInstanceOf[Long].toInt
               case lit =>
-                throw new ParseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
+                throw parseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
             }
 
             val fields = arguments.tail.map(arg => getFieldReference(applyCtx, 
arg))
@@ -960,7 +973,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         .map(typedVisit[Literal])
         .map(lit => LiteralValue(lit.value, lit.dataType))
       reference.orElse(literal)
-        .getOrElse(throw new ParseException("Invalid transform argument", ctx))
+        .getOrElse(throw parseException("Invalid transform argument", ctx))
     }
   }
 
@@ -970,13 +983,13 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     val legacyOn = conf.getConf(SQLConf.LEGACY_PROPERTY_NON_RESERVED)
     properties.filter {
       case (PROP_PROVIDER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_PROVIDER is a reserved table 
property, please use the USING clause to specify it.", ctx)
+        throw parseException(s"$PROP_PROVIDER is a reserved table property, 
please use the USING clause to specify it.", ctx)
       case (PROP_PROVIDER, _) => false
       case (PROP_LOCATION, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_LOCATION is a reserved table 
property, please use the LOCATION clause to specify it.", ctx)
+        throw parseException(s"$PROP_LOCATION is a reserved table property, 
please use the LOCATION clause to specify it.", ctx)
       case (PROP_LOCATION, _) => false
       case (PROP_OWNER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_OWNER is a reserved table property, 
it will be set to the current user.", ctx)
+        throw parseException(s"$PROP_OWNER is a reserved table property, it 
will be set to the current user.", ctx)
       case (PROP_OWNER, _) => false
       case _ => true
     }
@@ -989,7 +1002,7 @@ class HoodieSpark3_3ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     var path = location
     val filtered = cleanTableProperties(ctx, options).filter {
       case (k, v) if k.equalsIgnoreCase("path") && path.nonEmpty =>
-        throw new ParseException(s"Duplicated table paths found: '${path.get}' 
and '$v'. LOCATION" +
+        throw parseException(s"Duplicated table paths found: '${path.get}' and 
'$v'. LOCATION" +
           s" and the case insensitive key 'path' in OPTIONS are all used to 
indicate the custom" +
           s" table path, you can only specify one of them.", ctx)
       case (k, v) if k.equalsIgnoreCase("path") =>
diff --git 
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_4ExtendedSqlAstBuilder.scala
 
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_4ExtendedSqlAstBuilder.scala
index 695cf3fe1462..2bb46bbaa784 100644
--- 
a/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_4ExtendedSqlAstBuilder.scala
+++ 
b/hudi-spark-datasource/hudi-spark3.4.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_4ExtendedSqlAstBuilder.scala
@@ -29,7 +29,7 @@ import org.apache.spark.sql.catalyst.analysis._
 import org.apache.spark.sql.catalyst.catalog.BucketSpec
 import org.apache.spark.sql.catalyst.expressions._
 import org.apache.spark.sql.catalyst.parser.{ParseException, ParserInterface}
-import 
org.apache.spark.sql.catalyst.parser.ParserUtils.{checkDuplicateClauses, 
checkDuplicateKeys, operationNotAllowed, source, string, stringWithoutUnescape, 
validate, withOrigin}
+import 
org.apache.spark.sql.catalyst.parser.ParserUtils.{checkDuplicateClauses, 
checkDuplicateKeys, command, operationNotAllowed, position, source, string, 
stringWithoutUnescape, validate, withOrigin}
 import org.apache.spark.sql.catalyst.plans._
 import org.apache.spark.sql.catalyst.plans.logical._
 import org.apache.spark.sql.catalyst.util.{DateTimeUtils, IntervalUtils}
@@ -57,6 +57,17 @@ import scala.collection.JavaConverters._
 class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, delegate: 
ParserInterface)
   extends HoodieSqlBaseBaseVisitor[AnyRef] with Logging {
 
+  /**
+   * Returns a [[ParseException]] carrying `message` as plain human-readable 
text, positioned at
+   * `ctx`. The (String, ParserRuleContext) constructor treats the string as 
an error class on
+   * Spark 3.4+, so use the message-based primary constructor (errorClass 
stays None) (#19450).
+   * The Spark 4.x builders render the same messages behind an "Operation not 
allowed: " prefix,
+   * so cross-profile test assertions must stay substring-based.
+   */
+  private def parseException(message: String, ctx: ParserRuleContext): 
ParseException = {
+    new ParseException(Option(command(ctx)), message, position(ctx.start), 
position(ctx.stop))
+  }
+
   protected def typedVisit[T](ctx: ParseTree): T = {
     ctx.accept(this).asInstanceOf[T]
   }
@@ -130,7 +141,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
 
     def toLiteral[T](f: UTF8String => Option[T], t: DataType): Literal = {
       f(UTF8String.fromString(value)).map(Literal(_, t)).getOrElse {
-        throw new ParseException(s"Cannot parse the $valueType value: $value", 
ctx)
+        throw parseException(s"Cannot parse the $valueType value: $value", ctx)
       }
     }
 
@@ -179,7 +190,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.stringToInterval(UTF8String.fromString(value))
           } catch {
             case e: IllegalArgumentException =>
-              val ex = new ParseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
+              val ex = parseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
               ex.setStackTrace(e.getStackTrace)
               throw ex
           }
@@ -196,12 +207,12 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           val padding = if (value.length % 2 != 0) "0" else ""
           Literal(DatatypeConverter.parseHexBinary(padding + value))
         case other =>
-          throw new ParseException(s"Literals of type '$other' are currently 
not supported.", ctx)
+          throw parseException(s"Literals of type '$other' are currently not 
supported.", ctx)
       }
     } catch {
       case e: IllegalArgumentException =>
         val message = Option(e.getMessage).getOrElse(s"Exception parsing 
$valueType")
-        throw new ParseException(message, ctx)
+        throw parseException(message, ctx)
     }
   }
 
@@ -274,13 +285,13 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     try {
       val rawBigDecimal = BigDecimal(rawStrippedQualifier)
       if (rawBigDecimal < minValue || rawBigDecimal > maxValue) {
-        throw new ParseException(s"Numeric literal $rawStrippedQualifier does 
not " +
+        throw parseException(s"Numeric literal $rawStrippedQualifier does not 
" +
           s"fit in range [$minValue, $maxValue] for type $typeName", ctx)
       }
       Literal(converter(rawStrippedQualifier))
     } catch {
       case e: NumberFormatException =>
-        throw new ParseException(e.getMessage, ctx)
+        throw parseException(e.getMessage, ctx)
     }
   }
 
@@ -338,7 +349,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       Literal(BigDecimal(raw).underlying())
     } catch {
       case e: AnalysisException =>
-        throw new ParseException(e.message, ctx)
+        throw parseException(e.message, ctx)
     }
   }
 
@@ -389,7 +400,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (yearMonthFields.nonEmpty) {
       if (dayTimeFields.nonEmpty) {
         val literalStr = source(ctx)
-        throw new ParseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
+        throw parseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
       }
       Literal(
         calendarInterval.months,
@@ -446,18 +457,20 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (ctx.errorCapturingMultiUnitsInterval != null) {
       val innerCtx = ctx.errorCapturingMultiUnitsInterval
       if (innerCtx.unitToUnitInterval != null) {
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
       }
       visitMultiUnitsInterval(innerCtx.multiUnitsInterval)
     } else if (ctx.errorCapturingUnitToUnitInterval != null) {
       val innerCtx = ctx.errorCapturingUnitToUnitInterval
       if (innerCtx.error1 != null || innerCtx.error2 != null) {
         val errorCtx = if (innerCtx.error1 != null) innerCtx.error1 else 
innerCtx.error2
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
       }
       visitUnitToUnitInterval(innerCtx.body)
     } else {
-      throw new ParseException("at least one time unit should be given for 
interval literal", ctx)
+      // Unreachable through Hudi's pruned grammar: a bare INTERVAL keyword 
binds to
+      // transformArgument's qualifiedName alternative first. Kept for parity 
with Spark.
+      throw parseException("at least one time unit should be given for 
interval literal", ctx)
     }
   }
 
@@ -479,7 +492,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             // units and become valid ones, e.g. '1 day 2 hour'.
             // Ideally, we only ensure the value parts don't contain any units 
here.
             if (value.exists(Character.isLetter)) {
-              throw new ParseException("Can only use numbers in the interval 
value part for" +
+              throw parseException("Can only use numbers in the interval value 
part for" +
                 s" multiple unit value pairs interval form, but got invalid 
value: $value", ctx)
             }
             if (values(i).MINUS() == null) {
@@ -498,7 +511,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         IntervalUtils.stringToInterval(UTF8String.concat(kvs: _*))
       } catch {
         case i: IllegalArgumentException =>
-          val e = new ParseException(i.getMessage, ctx)
+          val e = parseException(i.getMessage, ctx)
           e.setStackTrace(i.getStackTrace)
           throw e
       }
@@ -520,7 +533,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           }
         }
       }.getOrElse {
-        throw new ParseException("The value of from-to unit must be a string", 
ctx.intervalValue)
+        throw parseException("The value of from-to unit must be a string", 
ctx.intervalValue)
       }
       try {
         val from = ctx.from.getText.toLowerCase(Locale.ROOT)
@@ -533,12 +546,12 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.fromDayTimeString(value,
               DayTimeIntervalType.stringToField(from), 
DayTimeIntervalType.stringToField(to))
           case _ =>
-            throw new ParseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
+            throw parseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
         }
       } catch {
         // Handle Exceptions thrown by CalendarInterval
         case e: IllegalArgumentException =>
-          val pe = new ParseException(e.getMessage, ctx)
+          val pe = parseException(e.getMessage, ctx)
           pe.setStackTrace(e.getStackTrace)
           throw pe
       }
@@ -579,7 +592,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           
HoodieSchema.parseTypeDescriptor(ctx.getText).asInstanceOf[HoodieSchema.Vector]
         } catch {
           case e: IllegalArgumentException =>
-            throw new ParseException(s"Invalid VECTOR type: ${e.getMessage}", 
ctx)
+            throw parseException(s"Invalid VECTOR type: ${e.getMessage}", ctx)
         }
         val sparkElemType = vectorSchema.getVectorElementType match {
           case HoodieSchema.Vector.VectorElementType.FLOAT => FloatType
@@ -596,7 +609,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       case ("interval", Nil) => CalendarIntervalType
       case (dt, params) =>
         val dtStr = if (params.nonEmpty) s"$dt(${params.mkString(",")})" else 
dt
-        throw new ParseException(s"DataType $dtStr is not supported.", ctx)
+        throw parseException(s"DataType $dtStr is not supported.", ctx)
     }
   }
 
@@ -607,7 +620,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = YearMonthIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       YearMonthIntervalType(start, end)
     } else {
@@ -622,7 +635,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = DayTimeIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       DayTimeIntervalType(start, end)
     } else {
@@ -890,7 +903,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         case ref: FieldReference =>
           ref
         case nonRef =>
-          throw new ParseException(s"Expected a column reference for transform 
$name: $nonRef.describe", ctx)
+          throw parseException(s"Expected a column reference for transform 
$name: ${nonRef.describe}", ctx)
       }
     }
 
@@ -899,7 +912,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
                                  arguments: Seq[V2Expression]): FieldReference 
= {
       lazy val name: String = ctx.identifier.getText
       if (arguments.size > 1) {
-        throw new ParseException(s"Too many arguments for transform $name", 
ctx)
+        throw parseException(s"Too many arguments for transform $name", ctx)
       } else {
         getFieldReference(ctx, arguments.head)
       }
@@ -922,7 +935,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
               case LiteralValue(longValue, LongType) =>
                 longValue.asInstanceOf[Long].toInt
               case lit =>
-                throw new ParseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
+                throw parseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
             }
 
             val fields = arguments.tail.map(arg => getFieldReference(applyCtx, 
arg))
@@ -960,7 +973,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         .map(typedVisit[Literal])
         .map(lit => LiteralValue(lit.value, lit.dataType))
       reference.orElse(literal)
-        .getOrElse(throw new ParseException("Invalid transform argument", ctx))
+        .getOrElse(throw parseException("Invalid transform argument", ctx))
     }
   }
 
@@ -970,13 +983,13 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     val legacyOn = conf.getConf(SQLConf.LEGACY_PROPERTY_NON_RESERVED)
     properties.filter {
       case (PROP_PROVIDER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_PROVIDER is a reserved table 
property, please use the USING clause to specify it.", ctx)
+        throw parseException(s"$PROP_PROVIDER is a reserved table property, 
please use the USING clause to specify it.", ctx)
       case (PROP_PROVIDER, _) => false
       case (PROP_LOCATION, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_LOCATION is a reserved table 
property, please use the LOCATION clause to specify it.", ctx)
+        throw parseException(s"$PROP_LOCATION is a reserved table property, 
please use the LOCATION clause to specify it.", ctx)
       case (PROP_LOCATION, _) => false
       case (PROP_OWNER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_OWNER is a reserved table property, 
it will be set to the current user.", ctx)
+        throw parseException(s"$PROP_OWNER is a reserved table property, it 
will be set to the current user.", ctx)
       case (PROP_OWNER, _) => false
       case _ => true
     }
@@ -989,7 +1002,7 @@ class HoodieSpark3_4ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     var path = location
     val filtered = cleanTableProperties(ctx, options).filter {
       case (k, v) if k.equalsIgnoreCase("path") && path.nonEmpty =>
-        throw new ParseException(s"Duplicated table paths found: '${path.get}' 
and '$v'. LOCATION" +
+        throw parseException(s"Duplicated table paths found: '${path.get}' and 
'$v'. LOCATION" +
           s" and the case insensitive key 'path' in OPTIONS are all used to 
indicate the custom" +
           s" table path, you can only specify one of them.", ctx)
       case (k, v) if k.equalsIgnoreCase("path") =>
diff --git 
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_5ExtendedSqlAstBuilder.scala
 
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_5ExtendedSqlAstBuilder.scala
index 1cfa82c02136..eda95a8c09ec 100644
--- 
a/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_5ExtendedSqlAstBuilder.scala
+++ 
b/hudi-spark-datasource/hudi-spark3.5.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark3_5ExtendedSqlAstBuilder.scala
@@ -29,7 +29,7 @@ import org.apache.spark.sql.catalyst.analysis._
 import org.apache.spark.sql.catalyst.catalog.BucketSpec
 import org.apache.spark.sql.catalyst.expressions._
 import org.apache.spark.sql.catalyst.parser.{ParseException, ParserInterface}
-import 
org.apache.spark.sql.catalyst.parser.ParserUtils.{checkDuplicateClauses, 
checkDuplicateKeys, operationNotAllowed, source, string, stringWithoutUnescape, 
validate, withOrigin}
+import 
org.apache.spark.sql.catalyst.parser.ParserUtils.{checkDuplicateClauses, 
checkDuplicateKeys, command, operationNotAllowed, position, source, string, 
stringWithoutUnescape, validate, withOrigin}
 import org.apache.spark.sql.catalyst.plans._
 import org.apache.spark.sql.catalyst.plans.logical._
 import org.apache.spark.sql.catalyst.util.{DateTimeUtils, IntervalUtils}
@@ -57,6 +57,17 @@ import scala.collection.JavaConverters._
 class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, delegate: 
ParserInterface)
   extends HoodieSqlBaseBaseVisitor[AnyRef] with Logging {
 
+  /**
+   * Returns a [[ParseException]] carrying `message` as plain human-readable 
text, positioned at
+   * `ctx`. The (String, ParserRuleContext) constructor treats the string as 
an error class on
+   * Spark 3.4+, so use the message-based primary constructor (errorClass 
stays None) (#19450).
+   * The Spark 4.x builders render the same messages behind an "Operation not 
allowed: " prefix,
+   * so cross-profile test assertions must stay substring-based.
+   */
+  private def parseException(message: String, ctx: ParserRuleContext): 
ParseException = {
+    new ParseException(Option(command(ctx)), message, position(ctx.start), 
position(ctx.stop))
+  }
+
   protected def typedVisit[T](ctx: ParseTree): T = {
     ctx.accept(this).asInstanceOf[T]
   }
@@ -130,7 +141,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
 
     def toLiteral[T](f: UTF8String => Option[T], t: DataType): Literal = {
       f(UTF8String.fromString(value)).map(Literal(_, t)).getOrElse {
-        throw new ParseException(s"Cannot parse the $valueType value: $value", 
ctx)
+        throw parseException(s"Cannot parse the $valueType value: $value", ctx)
       }
     }
 
@@ -179,7 +190,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.stringToInterval(UTF8String.fromString(value))
           } catch {
             case e: IllegalArgumentException =>
-              val ex = new ParseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
+              val ex = parseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
               ex.setStackTrace(e.getStackTrace)
               throw ex
           }
@@ -196,12 +207,12 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           val padding = if (value.length % 2 != 0) "0" else ""
           Literal(DatatypeConverter.parseHexBinary(padding + value))
         case other =>
-          throw new ParseException(s"Literals of type '$other' are currently 
not supported.", ctx)
+          throw parseException(s"Literals of type '$other' are currently not 
supported.", ctx)
       }
     } catch {
       case e: IllegalArgumentException =>
         val message = Option(e.getMessage).getOrElse(s"Exception parsing 
$valueType")
-        throw new ParseException(message, ctx)
+        throw parseException(message, ctx)
     }
   }
 
@@ -274,13 +285,13 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     try {
       val rawBigDecimal = BigDecimal(rawStrippedQualifier)
       if (rawBigDecimal < minValue || rawBigDecimal > maxValue) {
-        throw new ParseException(s"Numeric literal $rawStrippedQualifier does 
not " +
+        throw parseException(s"Numeric literal $rawStrippedQualifier does not 
" +
           s"fit in range [$minValue, $maxValue] for type $typeName", ctx)
       }
       Literal(converter(rawStrippedQualifier))
     } catch {
       case e: NumberFormatException =>
-        throw new ParseException(e.getMessage, ctx)
+        throw parseException(e.getMessage, ctx)
     }
   }
 
@@ -338,7 +349,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       Literal(BigDecimal(raw).underlying())
     } catch {
       case e: AnalysisException =>
-        throw new ParseException(e.message, ctx)
+        throw parseException(e.message, ctx)
     }
   }
 
@@ -389,7 +400,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (yearMonthFields.nonEmpty) {
       if (dayTimeFields.nonEmpty) {
         val literalStr = source(ctx)
-        throw new ParseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
+        throw parseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
       }
       Literal(
         calendarInterval.months,
@@ -446,18 +457,20 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (ctx.errorCapturingMultiUnitsInterval != null) {
       val innerCtx = ctx.errorCapturingMultiUnitsInterval
       if (innerCtx.unitToUnitInterval != null) {
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
       }
       visitMultiUnitsInterval(innerCtx.multiUnitsInterval)
     } else if (ctx.errorCapturingUnitToUnitInterval != null) {
       val innerCtx = ctx.errorCapturingUnitToUnitInterval
       if (innerCtx.error1 != null || innerCtx.error2 != null) {
         val errorCtx = if (innerCtx.error1 != null) innerCtx.error1 else 
innerCtx.error2
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
       }
       visitUnitToUnitInterval(innerCtx.body)
     } else {
-      throw new ParseException("at least one time unit should be given for 
interval literal", ctx)
+      // Unreachable through Hudi's pruned grammar: a bare INTERVAL keyword 
binds to
+      // transformArgument's qualifiedName alternative first. Kept for parity 
with Spark.
+      throw parseException("at least one time unit should be given for 
interval literal", ctx)
     }
   }
 
@@ -479,7 +492,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             // units and become valid ones, e.g. '1 day 2 hour'.
             // Ideally, we only ensure the value parts don't contain any units 
here.
             if (value.exists(Character.isLetter)) {
-              throw new ParseException("Can only use numbers in the interval 
value part for" +
+              throw parseException("Can only use numbers in the interval value 
part for" +
                 s" multiple unit value pairs interval form, but got invalid 
value: $value", ctx)
             }
             if (values(i).MINUS() == null) {
@@ -498,7 +511,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         IntervalUtils.stringToInterval(UTF8String.concat(kvs: _*))
       } catch {
         case i: IllegalArgumentException =>
-          val e = new ParseException(i.getMessage, ctx)
+          val e = parseException(i.getMessage, ctx)
           e.setStackTrace(i.getStackTrace)
           throw e
       }
@@ -520,7 +533,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           }
         }
       }.getOrElse {
-        throw new ParseException("The value of from-to unit must be a string", 
ctx.intervalValue)
+        throw parseException("The value of from-to unit must be a string", 
ctx.intervalValue)
       }
       try {
         val from = ctx.from.getText.toLowerCase(Locale.ROOT)
@@ -533,12 +546,12 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.fromDayTimeString(value,
               DayTimeIntervalType.stringToField(from), 
DayTimeIntervalType.stringToField(to))
           case _ =>
-            throw new ParseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
+            throw parseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
         }
       } catch {
         // Handle Exceptions thrown by CalendarInterval
         case e: IllegalArgumentException =>
-          val pe = new ParseException(e.getMessage, ctx)
+          val pe = parseException(e.getMessage, ctx)
           pe.setStackTrace(e.getStackTrace)
           throw pe
       }
@@ -579,7 +592,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           
HoodieSchema.parseTypeDescriptor(ctx.getText).asInstanceOf[HoodieSchema.Vector]
         } catch {
           case e: IllegalArgumentException =>
-            throw new ParseException(s"Invalid VECTOR type: ${e.getMessage}", 
ctx)
+            throw parseException(s"Invalid VECTOR type: ${e.getMessage}", ctx)
         }
         val sparkElemType = vectorSchema.getVectorElementType match {
           case HoodieSchema.Vector.VectorElementType.FLOAT => FloatType
@@ -596,7 +609,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       case ("interval", Nil) => CalendarIntervalType
       case (dt, params) =>
         val dtStr = if (params.nonEmpty) s"$dt(${params.mkString(",")})" else 
dt
-        throw new ParseException(s"DataType $dtStr is not supported.", ctx)
+        throw parseException(s"DataType $dtStr is not supported.", ctx)
     }
   }
 
@@ -607,7 +620,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = YearMonthIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       YearMonthIntervalType(start, end)
     } else {
@@ -622,7 +635,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = DayTimeIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       DayTimeIntervalType(start, end)
     } else {
@@ -890,7 +903,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         case ref: FieldReference =>
           ref
         case nonRef =>
-          throw new ParseException(s"Expected a column reference for transform 
$name: $nonRef.describe", ctx)
+          throw parseException(s"Expected a column reference for transform 
$name: ${nonRef.describe}", ctx)
       }
     }
 
@@ -899,7 +912,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
                                  arguments: Seq[V2Expression]): FieldReference 
= {
       lazy val name: String = ctx.identifier.getText
       if (arguments.size > 1) {
-        throw new ParseException(s"Too many arguments for transform $name", 
ctx)
+        throw parseException(s"Too many arguments for transform $name", ctx)
       } else {
         getFieldReference(ctx, arguments.head)
       }
@@ -922,7 +935,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
               case LiteralValue(longValue, LongType) =>
                 longValue.asInstanceOf[Long].toInt
               case lit =>
-                throw new ParseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
+                throw parseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
             }
 
             val fields = arguments.tail.map(arg => getFieldReference(applyCtx, 
arg))
@@ -960,7 +973,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         .map(typedVisit[Literal])
         .map(lit => LiteralValue(lit.value, lit.dataType))
       reference.orElse(literal)
-        .getOrElse(throw new ParseException("Invalid transform argument", ctx))
+        .getOrElse(throw parseException("Invalid transform argument", ctx))
     }
   }
 
@@ -970,13 +983,13 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     val legacyOn = conf.getConf(SQLConf.LEGACY_PROPERTY_NON_RESERVED)
     properties.filter {
       case (PROP_PROVIDER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_PROVIDER is a reserved table 
property, please use the USING clause to specify it.", ctx)
+        throw parseException(s"$PROP_PROVIDER is a reserved table property, 
please use the USING clause to specify it.", ctx)
       case (PROP_PROVIDER, _) => false
       case (PROP_LOCATION, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_LOCATION is a reserved table 
property, please use the LOCATION clause to specify it.", ctx)
+        throw parseException(s"$PROP_LOCATION is a reserved table property, 
please use the LOCATION clause to specify it.", ctx)
       case (PROP_LOCATION, _) => false
       case (PROP_OWNER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_OWNER is a reserved table property, 
it will be set to the current user.", ctx)
+        throw parseException(s"$PROP_OWNER is a reserved table property, it 
will be set to the current user.", ctx)
       case (PROP_OWNER, _) => false
       case _ => true
     }
@@ -989,7 +1002,7 @@ class HoodieSpark3_5ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     var path = location
     val filtered = cleanTableProperties(ctx, options).filter {
       case (k, v) if k.equalsIgnoreCase("path") && path.nonEmpty =>
-        throw new ParseException(s"Duplicated table paths found: '${path.get}' 
and '$v'. LOCATION" +
+        throw parseException(s"Duplicated table paths found: '${path.get}' and 
'$v'. LOCATION" +
           s" and the case insensitive key 'path' in OPTIONS are all used to 
indicate the custom" +
           s" table path, you can only specify one of them.", ctx)
       case (k, v) if k.equalsIgnoreCase("path") =>
diff --git 
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_0ExtendedSqlAstBuilder.scala
 
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_0ExtendedSqlAstBuilder.scala
index 08863dc81a33..81982a2e9857 100644
--- 
a/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_0ExtendedSqlAstBuilder.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.0.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_0ExtendedSqlAstBuilder.scala
@@ -57,6 +57,17 @@ import scala.collection.JavaConverters._
 class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, delegate: 
ParserInterface)
   extends HoodieSqlBaseBaseVisitor[AnyRef] with Logging {
 
+  /**
+   * Returns a [[ParseException]] carrying `message` as human-readable text, 
positioned at `ctx`.
+   * Spark 4.x only exposes error-class constructors, so route the message 
through the class that
+   * ParserUtils.operationNotAllowed uses; output gains an "Operation not 
allowed: " prefix (the
+   * trailing period is stripped to avoid doubling). If Spark drops 
_LEGACY_ERROR_TEMP_0035, the
+   * intercept[ParseException] cases in TestBlobDataType fail loudly on the 
4.x profiles (#19450).
+   */
+  private def parseException(message: String, ctx: ParserRuleContext): 
ParseException = {
+    new ParseException("_LEGACY_ERROR_TEMP_0035", Map("message" -> 
message.stripSuffix(".")), ctx)
+  }
+
   protected def typedVisit[T](ctx: ParseTree): T = {
     ctx.accept(this).asInstanceOf[T]
   }
@@ -130,7 +141,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
 
     def toLiteral[T](f: UTF8String => Option[T], t: DataType): Literal = {
       f(UTF8String.fromString(value)).map(Literal(_, t)).getOrElse {
-        throw new ParseException(s"Cannot parse the $valueType value: $value", 
ctx)
+        throw parseException(s"Cannot parse the $valueType value: $value", ctx)
       }
     }
 
@@ -179,7 +190,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.stringToInterval(UTF8String.fromString(value))
           } catch {
             case e: IllegalArgumentException =>
-              val ex = new ParseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
+              val ex = parseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
               ex.setStackTrace(e.getStackTrace)
               throw ex
           }
@@ -196,12 +207,12 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           val padding = if (value.length % 2 != 0) "0" else ""
           Literal(DatatypeConverter.parseHexBinary(padding + value))
         case other =>
-          throw new ParseException(s"Literals of type '$other' are currently 
not supported.", ctx)
+          throw parseException(s"Literals of type '$other' are currently not 
supported.", ctx)
       }
     } catch {
       case e: IllegalArgumentException =>
         val message = Option(e.getMessage).getOrElse(s"Exception parsing 
$valueType")
-        throw new ParseException(message, ctx)
+        throw parseException(message, ctx)
     }
   }
 
@@ -274,13 +285,13 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     try {
       val rawBigDecimal = BigDecimal(rawStrippedQualifier)
       if (rawBigDecimal < minValue || rawBigDecimal > maxValue) {
-        throw new ParseException(s"Numeric literal $rawStrippedQualifier does 
not " +
+        throw parseException(s"Numeric literal $rawStrippedQualifier does not 
" +
           s"fit in range [$minValue, $maxValue] for type $typeName", ctx)
       }
       Literal(converter(rawStrippedQualifier))
     } catch {
       case e: NumberFormatException =>
-        throw new ParseException(e.getMessage, ctx)
+        throw parseException(e.getMessage, ctx)
     }
   }
 
@@ -338,7 +349,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       Literal(BigDecimal(raw).underlying())
     } catch {
       case e: AnalysisException =>
-        throw new ParseException(e.message, ctx)
+        throw parseException(e.message, ctx)
     }
   }
 
@@ -389,7 +400,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (yearMonthFields.nonEmpty) {
       if (dayTimeFields.nonEmpty) {
         val literalStr = source(ctx)
-        throw new ParseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
+        throw parseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
       }
       Literal(
         calendarInterval.months,
@@ -446,18 +457,20 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (ctx.errorCapturingMultiUnitsInterval != null) {
       val innerCtx = ctx.errorCapturingMultiUnitsInterval
       if (innerCtx.unitToUnitInterval != null) {
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
       }
       visitMultiUnitsInterval(innerCtx.multiUnitsInterval)
     } else if (ctx.errorCapturingUnitToUnitInterval != null) {
       val innerCtx = ctx.errorCapturingUnitToUnitInterval
       if (innerCtx.error1 != null || innerCtx.error2 != null) {
         val errorCtx = if (innerCtx.error1 != null) innerCtx.error1 else 
innerCtx.error2
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
       }
       visitUnitToUnitInterval(innerCtx.body)
     } else {
-      throw new ParseException("at least one time unit should be given for 
interval literal", ctx)
+      // Unreachable through Hudi's pruned grammar: a bare INTERVAL keyword 
binds to
+      // transformArgument's qualifiedName alternative first. Kept for parity 
with Spark.
+      throw parseException("at least one time unit should be given for 
interval literal", ctx)
     }
   }
 
@@ -479,7 +492,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             // units and become valid ones, e.g. '1 day 2 hour'.
             // Ideally, we only ensure the value parts don't contain any units 
here.
             if (value.exists(Character.isLetter)) {
-              throw new ParseException("Can only use numbers in the interval 
value part for" +
+              throw parseException("Can only use numbers in the interval value 
part for" +
                 s" multiple unit value pairs interval form, but got invalid 
value: $value", ctx)
             }
             if (values(i).MINUS() == null) {
@@ -498,7 +511,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         IntervalUtils.stringToInterval(UTF8String.concat(kvs: _*))
       } catch {
         case i: IllegalArgumentException =>
-          val e = new ParseException(i.getMessage, ctx)
+          val e = parseException(i.getMessage, ctx)
           e.setStackTrace(i.getStackTrace)
           throw e
       }
@@ -520,7 +533,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           }
         }
       }.getOrElse {
-        throw new ParseException("The value of from-to unit must be a string", 
ctx.intervalValue)
+        throw parseException("The value of from-to unit must be a string", 
ctx.intervalValue)
       }
       try {
         val from = ctx.from.getText.toLowerCase(Locale.ROOT)
@@ -533,12 +546,12 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.fromDayTimeString(value,
               DayTimeIntervalType.stringToField(from), 
DayTimeIntervalType.stringToField(to))
           case _ =>
-            throw new ParseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
+            throw parseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
         }
       } catch {
         // Handle Exceptions thrown by CalendarInterval
         case e: IllegalArgumentException =>
-          val pe = new ParseException(e.getMessage, ctx)
+          val pe = parseException(e.getMessage, ctx)
           pe.setStackTrace(e.getStackTrace)
           throw pe
       }
@@ -579,7 +592,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           
HoodieSchema.parseTypeDescriptor(ctx.getText).asInstanceOf[HoodieSchema.Vector]
         } catch {
           case e: IllegalArgumentException =>
-            throw new ParseException(s"Invalid VECTOR type: ${e.getMessage}", 
ctx)
+            throw parseException(s"Invalid VECTOR type: ${e.getMessage}", ctx)
         }
         val sparkElemType = vectorSchema.getVectorElementType match {
           case HoodieSchema.Vector.VectorElementType.FLOAT => FloatType
@@ -596,7 +609,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       case ("interval", Nil) => CalendarIntervalType
       case (dt, params) =>
         val dtStr = if (params.nonEmpty) s"$dt(${params.mkString(",")})" else 
dt
-        throw new ParseException(s"DataType $dtStr is not supported.", ctx)
+        throw parseException(s"DataType $dtStr is not supported.", ctx)
     }
   }
 
@@ -607,7 +620,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = YearMonthIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       YearMonthIntervalType(start, end)
     } else {
@@ -622,7 +635,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = DayTimeIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       DayTimeIntervalType(start, end)
     } else {
@@ -890,7 +903,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         case ref: FieldReference =>
           ref
         case nonRef =>
-          throw new ParseException(s"Expected a column reference for transform 
$name: $nonRef.describe", ctx)
+          throw parseException(s"Expected a column reference for transform 
$name: ${nonRef.describe}", ctx)
       }
     }
 
@@ -899,7 +912,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
                                  arguments: Seq[V2Expression]): FieldReference 
= {
       lazy val name: String = ctx.identifier.getText
       if (arguments.size > 1) {
-        throw new ParseException(s"Too many arguments for transform $name", 
ctx)
+        throw parseException(s"Too many arguments for transform $name", ctx)
       } else {
         getFieldReference(ctx, arguments.head)
       }
@@ -922,7 +935,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
               case LiteralValue(longValue, LongType) =>
                 longValue.asInstanceOf[Long].toInt
               case lit =>
-                throw new ParseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
+                throw parseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
             }
 
             val fields = arguments.tail.map(arg => getFieldReference(applyCtx, 
arg))
@@ -960,7 +973,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         .map(typedVisit[Literal])
         .map(lit => LiteralValue(lit.value, lit.dataType))
       reference.orElse(literal)
-        .getOrElse(throw new ParseException("Invalid transform argument", ctx))
+        .getOrElse(throw parseException("Invalid transform argument", ctx))
     }
   }
 
@@ -970,13 +983,13 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     val legacyOn = conf.getConf(SQLConf.LEGACY_PROPERTY_NON_RESERVED)
     properties.filter {
       case (PROP_PROVIDER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_PROVIDER is a reserved table 
property, please use the USING clause to specify it.", ctx)
+        throw parseException(s"$PROP_PROVIDER is a reserved table property, 
please use the USING clause to specify it.", ctx)
       case (PROP_PROVIDER, _) => false
       case (PROP_LOCATION, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_LOCATION is a reserved table 
property, please use the LOCATION clause to specify it.", ctx)
+        throw parseException(s"$PROP_LOCATION is a reserved table property, 
please use the LOCATION clause to specify it.", ctx)
       case (PROP_LOCATION, _) => false
       case (PROP_OWNER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_OWNER is a reserved table property, 
it will be set to the current user.", ctx)
+        throw parseException(s"$PROP_OWNER is a reserved table property, it 
will be set to the current user.", ctx)
       case (PROP_OWNER, _) => false
       case _ => true
     }
@@ -989,7 +1002,7 @@ class HoodieSpark4_0ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     var path = location
     val filtered = cleanTableProperties(ctx, options).filter {
       case (k, v) if k.equalsIgnoreCase("path") && path.nonEmpty =>
-        throw new ParseException(s"Duplicated table paths found: '${path.get}' 
and '$v'. LOCATION" +
+        throw parseException(s"Duplicated table paths found: '${path.get}' and 
'$v'. LOCATION" +
           s" and the case insensitive key 'path' in OPTIONS are all used to 
indicate the custom" +
           s" table path, you can only specify one of them.", ctx)
       case (k, v) if k.equalsIgnoreCase("path") =>
diff --git 
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_1ExtendedSqlAstBuilder.scala
 
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_1ExtendedSqlAstBuilder.scala
index 74b1192bef6c..008ac8d92b6e 100644
--- 
a/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_1ExtendedSqlAstBuilder.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.1.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_1ExtendedSqlAstBuilder.scala
@@ -57,6 +57,17 @@ import scala.collection.JavaConverters._
 class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, delegate: 
ParserInterface)
   extends HoodieSqlBaseBaseVisitor[AnyRef] with Logging {
 
+  /**
+   * Returns a [[ParseException]] carrying `message` as human-readable text, 
positioned at `ctx`.
+   * Spark 4.x only exposes error-class constructors, so route the message 
through the class that
+   * ParserUtils.operationNotAllowed uses; output gains an "Operation not 
allowed: " prefix (the
+   * trailing period is stripped to avoid doubling). If Spark drops 
_LEGACY_ERROR_TEMP_0035, the
+   * intercept[ParseException] cases in TestBlobDataType fail loudly on the 
4.x profiles (#19450).
+   */
+  private def parseException(message: String, ctx: ParserRuleContext): 
ParseException = {
+    new ParseException("_LEGACY_ERROR_TEMP_0035", Map("message" -> 
message.stripSuffix(".")), ctx)
+  }
+
   protected def typedVisit[T](ctx: ParseTree): T = {
     ctx.accept(this).asInstanceOf[T]
   }
@@ -130,7 +141,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
 
     def toLiteral[T](f: UTF8String => Option[T], t: DataType): Literal = {
       f(UTF8String.fromString(value)).map(Literal(_, t)).getOrElse {
-        throw new ParseException(s"Cannot parse the $valueType value: $value", 
ctx)
+        throw parseException(s"Cannot parse the $valueType value: $value", ctx)
       }
     }
 
@@ -179,7 +190,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.stringToInterval(UTF8String.fromString(value))
           } catch {
             case e: IllegalArgumentException =>
-              val ex = new ParseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
+              val ex = parseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
               ex.setStackTrace(e.getStackTrace)
               throw ex
           }
@@ -196,12 +207,12 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           val padding = if (value.length % 2 != 0) "0" else ""
           Literal(DatatypeConverter.parseHexBinary(padding + value))
         case other =>
-          throw new ParseException(s"Literals of type '$other' are currently 
not supported.", ctx)
+          throw parseException(s"Literals of type '$other' are currently not 
supported.", ctx)
       }
     } catch {
       case e: IllegalArgumentException =>
         val message = Option(e.getMessage).getOrElse(s"Exception parsing 
$valueType")
-        throw new ParseException(message, ctx)
+        throw parseException(message, ctx)
     }
   }
 
@@ -274,13 +285,13 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     try {
       val rawBigDecimal = BigDecimal(rawStrippedQualifier)
       if (rawBigDecimal < minValue || rawBigDecimal > maxValue) {
-        throw new ParseException(s"Numeric literal $rawStrippedQualifier does 
not " +
+        throw parseException(s"Numeric literal $rawStrippedQualifier does not 
" +
           s"fit in range [$minValue, $maxValue] for type $typeName", ctx)
       }
       Literal(converter(rawStrippedQualifier))
     } catch {
       case e: NumberFormatException =>
-        throw new ParseException(e.getMessage, ctx)
+        throw parseException(e.getMessage, ctx)
     }
   }
 
@@ -338,7 +349,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       Literal(BigDecimal(raw).underlying())
     } catch {
       case e: AnalysisException =>
-        throw new ParseException(e.message, ctx)
+        throw parseException(e.message, ctx)
     }
   }
 
@@ -389,7 +400,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (yearMonthFields.nonEmpty) {
       if (dayTimeFields.nonEmpty) {
         val literalStr = source(ctx)
-        throw new ParseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
+        throw parseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
       }
       Literal(
         calendarInterval.months,
@@ -446,18 +457,20 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (ctx.errorCapturingMultiUnitsInterval != null) {
       val innerCtx = ctx.errorCapturingMultiUnitsInterval
       if (innerCtx.unitToUnitInterval != null) {
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
       }
       visitMultiUnitsInterval(innerCtx.multiUnitsInterval)
     } else if (ctx.errorCapturingUnitToUnitInterval != null) {
       val innerCtx = ctx.errorCapturingUnitToUnitInterval
       if (innerCtx.error1 != null || innerCtx.error2 != null) {
         val errorCtx = if (innerCtx.error1 != null) innerCtx.error1 else 
innerCtx.error2
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
       }
       visitUnitToUnitInterval(innerCtx.body)
     } else {
-      throw new ParseException("at least one time unit should be given for 
interval literal", ctx)
+      // Unreachable through Hudi's pruned grammar: a bare INTERVAL keyword 
binds to
+      // transformArgument's qualifiedName alternative first. Kept for parity 
with Spark.
+      throw parseException("at least one time unit should be given for 
interval literal", ctx)
     }
   }
 
@@ -479,7 +492,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             // units and become valid ones, e.g. '1 day 2 hour'.
             // Ideally, we only ensure the value parts don't contain any units 
here.
             if (value.exists(Character.isLetter)) {
-              throw new ParseException("Can only use numbers in the interval 
value part for" +
+              throw parseException("Can only use numbers in the interval value 
part for" +
                 s" multiple unit value pairs interval form, but got invalid 
value: $value", ctx)
             }
             if (values(i).MINUS() == null) {
@@ -498,7 +511,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         IntervalUtils.stringToInterval(UTF8String.concat(kvs: _*))
       } catch {
         case i: IllegalArgumentException =>
-          val e = new ParseException(i.getMessage, ctx)
+          val e = parseException(i.getMessage, ctx)
           e.setStackTrace(i.getStackTrace)
           throw e
       }
@@ -520,7 +533,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           }
         }
       }.getOrElse {
-        throw new ParseException("The value of from-to unit must be a string", 
ctx.intervalValue)
+        throw parseException("The value of from-to unit must be a string", 
ctx.intervalValue)
       }
       try {
         val from = ctx.from.getText.toLowerCase(Locale.ROOT)
@@ -533,12 +546,12 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.fromDayTimeString(value,
               DayTimeIntervalType.stringToField(from), 
DayTimeIntervalType.stringToField(to))
           case _ =>
-            throw new ParseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
+            throw parseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
         }
       } catch {
         // Handle Exceptions thrown by CalendarInterval
         case e: IllegalArgumentException =>
-          val pe = new ParseException(e.getMessage, ctx)
+          val pe = parseException(e.getMessage, ctx)
           pe.setStackTrace(e.getStackTrace)
           throw pe
       }
@@ -579,7 +592,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           
HoodieSchema.parseTypeDescriptor(ctx.getText).asInstanceOf[HoodieSchema.Vector]
         } catch {
           case e: IllegalArgumentException =>
-            throw new ParseException(s"Invalid VECTOR type: ${e.getMessage}", 
ctx)
+            throw parseException(s"Invalid VECTOR type: ${e.getMessage}", ctx)
         }
         val sparkElemType = vectorSchema.getVectorElementType match {
           case HoodieSchema.Vector.VectorElementType.FLOAT => FloatType
@@ -596,7 +609,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       case ("interval", Nil) => CalendarIntervalType
       case (dt, params) =>
         val dtStr = if (params.nonEmpty) s"$dt(${params.mkString(",")})" else 
dt
-        throw new ParseException(s"DataType $dtStr is not supported.", ctx)
+        throw parseException(s"DataType $dtStr is not supported.", ctx)
     }
   }
 
@@ -607,7 +620,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = YearMonthIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       YearMonthIntervalType(start, end)
     } else {
@@ -622,7 +635,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = DayTimeIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       DayTimeIntervalType(start, end)
     } else {
@@ -890,7 +903,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         case ref: FieldReference =>
           ref
         case nonRef =>
-          throw new ParseException(s"Expected a column reference for transform 
$name: $nonRef.describe", ctx)
+          throw parseException(s"Expected a column reference for transform 
$name: ${nonRef.describe}", ctx)
       }
     }
 
@@ -899,7 +912,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
                                  arguments: Seq[V2Expression]): FieldReference 
= {
       lazy val name: String = ctx.identifier.getText
       if (arguments.size > 1) {
-        throw new ParseException(s"Too many arguments for transform $name", 
ctx)
+        throw parseException(s"Too many arguments for transform $name", ctx)
       } else {
         getFieldReference(ctx, arguments.head)
       }
@@ -922,7 +935,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
               case LiteralValue(longValue, LongType) =>
                 longValue.asInstanceOf[Long].toInt
               case lit =>
-                throw new ParseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
+                throw parseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
             }
 
             val fields = arguments.tail.map(arg => getFieldReference(applyCtx, 
arg))
@@ -960,7 +973,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         .map(typedVisit[Literal])
         .map(lit => LiteralValue(lit.value, lit.dataType))
       reference.orElse(literal)
-        .getOrElse(throw new ParseException("Invalid transform argument", ctx))
+        .getOrElse(throw parseException("Invalid transform argument", ctx))
     }
   }
 
@@ -970,13 +983,13 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     val legacyOn = conf.getConf(SQLConf.LEGACY_PROPERTY_NON_RESERVED)
     properties.filter {
       case (PROP_PROVIDER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_PROVIDER is a reserved table 
property, please use the USING clause to specify it.", ctx)
+        throw parseException(s"$PROP_PROVIDER is a reserved table property, 
please use the USING clause to specify it.", ctx)
       case (PROP_PROVIDER, _) => false
       case (PROP_LOCATION, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_LOCATION is a reserved table 
property, please use the LOCATION clause to specify it.", ctx)
+        throw parseException(s"$PROP_LOCATION is a reserved table property, 
please use the LOCATION clause to specify it.", ctx)
       case (PROP_LOCATION, _) => false
       case (PROP_OWNER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_OWNER is a reserved table property, 
it will be set to the current user.", ctx)
+        throw parseException(s"$PROP_OWNER is a reserved table property, it 
will be set to the current user.", ctx)
       case (PROP_OWNER, _) => false
       case _ => true
     }
@@ -989,7 +1002,7 @@ class HoodieSpark4_1ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     var path = location
     val filtered = cleanTableProperties(ctx, options).filter {
       case (k, v) if k.equalsIgnoreCase("path") && path.nonEmpty =>
-        throw new ParseException(s"Duplicated table paths found: '${path.get}' 
and '$v'. LOCATION" +
+        throw parseException(s"Duplicated table paths found: '${path.get}' and 
'$v'. LOCATION" +
           s" and the case insensitive key 'path' in OPTIONS are all used to 
indicate the custom" +
           s" table path, you can only specify one of them.", ctx)
       case (k, v) if k.equalsIgnoreCase("path") =>
diff --git 
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_2ExtendedSqlAstBuilder.scala
 
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_2ExtendedSqlAstBuilder.scala
index 94066110b720..d6442fd01144 100644
--- 
a/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_2ExtendedSqlAstBuilder.scala
+++ 
b/hudi-spark-datasource/hudi-spark4.2.x/src/main/scala/org/apache/spark/sql/parser/HoodieSpark4_2ExtendedSqlAstBuilder.scala
@@ -57,6 +57,17 @@ import scala.collection.JavaConverters._
 class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, delegate: 
ParserInterface)
   extends HoodieSqlBaseBaseVisitor[AnyRef] with Logging {
 
+  /**
+   * Returns a [[ParseException]] carrying `message` as human-readable text, 
positioned at `ctx`.
+   * Spark 4.x only exposes error-class constructors, so route the message 
through the class that
+   * ParserUtils.operationNotAllowed uses; output gains an "Operation not 
allowed: " prefix (the
+   * trailing period is stripped to avoid doubling). If Spark drops 
_LEGACY_ERROR_TEMP_0035, the
+   * intercept[ParseException] cases in TestBlobDataType fail loudly on the 
4.x profiles (#19450).
+   */
+  private def parseException(message: String, ctx: ParserRuleContext): 
ParseException = {
+    new ParseException("_LEGACY_ERROR_TEMP_0035", Map("message" -> 
message.stripSuffix(".")), ctx)
+  }
+
   protected def typedVisit[T](ctx: ParseTree): T = {
     ctx.accept(this).asInstanceOf[T]
   }
@@ -130,7 +141,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
 
     def toLiteral[T](f: UTF8String => Option[T], t: DataType): Literal = {
       f(UTF8String.fromString(value)).map(Literal(_, t)).getOrElse {
-        throw new ParseException(s"Cannot parse the $valueType value: $value", 
ctx)
+        throw parseException(s"Cannot parse the $valueType value: $value", ctx)
       }
     }
 
@@ -179,7 +190,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.stringToInterval(UTF8String.fromString(value))
           } catch {
             case e: IllegalArgumentException =>
-              val ex = new ParseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
+              val ex = parseException(s"Cannot parse the INTERVAL value: 
$value", ctx)
               ex.setStackTrace(e.getStackTrace)
               throw ex
           }
@@ -196,12 +207,12 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           val padding = if (value.length % 2 != 0) "0" else ""
           Literal(DatatypeConverter.parseHexBinary(padding + value))
         case other =>
-          throw new ParseException(s"Literals of type '$other' are currently 
not supported.", ctx)
+          throw parseException(s"Literals of type '$other' are currently not 
supported.", ctx)
       }
     } catch {
       case e: IllegalArgumentException =>
         val message = Option(e.getMessage).getOrElse(s"Exception parsing 
$valueType")
-        throw new ParseException(message, ctx)
+        throw parseException(message, ctx)
     }
   }
 
@@ -274,13 +285,13 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     try {
       val rawBigDecimal = BigDecimal(rawStrippedQualifier)
       if (rawBigDecimal < minValue || rawBigDecimal > maxValue) {
-        throw new ParseException(s"Numeric literal $rawStrippedQualifier does 
not " +
+        throw parseException(s"Numeric literal $rawStrippedQualifier does not 
" +
           s"fit in range [$minValue, $maxValue] for type $typeName", ctx)
       }
       Literal(converter(rawStrippedQualifier))
     } catch {
       case e: NumberFormatException =>
-        throw new ParseException(e.getMessage, ctx)
+        throw parseException(e.getMessage, ctx)
     }
   }
 
@@ -338,7 +349,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       Literal(BigDecimal(raw).underlying())
     } catch {
       case e: AnalysisException =>
-        throw new ParseException(e.message, ctx)
+        throw parseException(e.message, ctx)
     }
   }
 
@@ -389,7 +400,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (yearMonthFields.nonEmpty) {
       if (dayTimeFields.nonEmpty) {
         val literalStr = source(ctx)
-        throw new ParseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
+        throw parseException(s"Cannot mix year-month and day-time fields: 
$literalStr", ctx)
       }
       Literal(
         calendarInterval.months,
@@ -446,18 +457,20 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     if (ctx.errorCapturingMultiUnitsInterval != null) {
       val innerCtx = ctx.errorCapturingMultiUnitsInterval
       if (innerCtx.unitToUnitInterval != null) {
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", innerCtx.unitToUnitInterval)
       }
       visitMultiUnitsInterval(innerCtx.multiUnitsInterval)
     } else if (ctx.errorCapturingUnitToUnitInterval != null) {
       val innerCtx = ctx.errorCapturingUnitToUnitInterval
       if (innerCtx.error1 != null || innerCtx.error2 != null) {
         val errorCtx = if (innerCtx.error1 != null) innerCtx.error1 else 
innerCtx.error2
-        throw new ParseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
+        throw parseException("Can only have a single from-to unit in the 
interval literal syntax", errorCtx)
       }
       visitUnitToUnitInterval(innerCtx.body)
     } else {
-      throw new ParseException("at least one time unit should be given for 
interval literal", ctx)
+      // Unreachable through Hudi's pruned grammar: a bare INTERVAL keyword 
binds to
+      // transformArgument's qualifiedName alternative first. Kept for parity 
with Spark.
+      throw parseException("at least one time unit should be given for 
interval literal", ctx)
     }
   }
 
@@ -479,7 +492,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             // units and become valid ones, e.g. '1 day 2 hour'.
             // Ideally, we only ensure the value parts don't contain any units 
here.
             if (value.exists(Character.isLetter)) {
-              throw new ParseException("Can only use numbers in the interval 
value part for" +
+              throw parseException("Can only use numbers in the interval value 
part for" +
                 s" multiple unit value pairs interval form, but got invalid 
value: $value", ctx)
             }
             if (values(i).MINUS() == null) {
@@ -498,7 +511,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         IntervalUtils.stringToInterval(UTF8String.concat(kvs: _*))
       } catch {
         case i: IllegalArgumentException =>
-          val e = new ParseException(i.getMessage, ctx)
+          val e = parseException(i.getMessage, ctx)
           e.setStackTrace(i.getStackTrace)
           throw e
       }
@@ -520,7 +533,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           }
         }
       }.getOrElse {
-        throw new ParseException("The value of from-to unit must be a string", 
ctx.intervalValue)
+        throw parseException("The value of from-to unit must be a string", 
ctx.intervalValue)
       }
       try {
         val from = ctx.from.getText.toLowerCase(Locale.ROOT)
@@ -533,12 +546,12 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
             IntervalUtils.fromDayTimeString(value,
               DayTimeIntervalType.stringToField(from), 
DayTimeIntervalType.stringToField(to))
           case _ =>
-            throw new ParseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
+            throw parseException(s"Intervals FROM $from TO $to are not 
supported.", ctx)
         }
       } catch {
         // Handle Exceptions thrown by CalendarInterval
         case e: IllegalArgumentException =>
-          val pe = new ParseException(e.getMessage, ctx)
+          val pe = parseException(e.getMessage, ctx)
           pe.setStackTrace(e.getStackTrace)
           throw pe
       }
@@ -579,7 +592,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
           
HoodieSchema.parseTypeDescriptor(ctx.getText).asInstanceOf[HoodieSchema.Vector]
         } catch {
           case e: IllegalArgumentException =>
-            throw new ParseException(s"Invalid VECTOR type: ${e.getMessage}", 
ctx)
+            throw parseException(s"Invalid VECTOR type: ${e.getMessage}", ctx)
         }
         val sparkElemType = vectorSchema.getVectorElementType match {
           case HoodieSchema.Vector.VectorElementType.FLOAT => FloatType
@@ -596,7 +609,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       case ("interval", Nil) => CalendarIntervalType
       case (dt, params) =>
         val dtStr = if (params.nonEmpty) s"$dt(${params.mkString(",")})" else 
dt
-        throw new ParseException(s"DataType $dtStr is not supported.", ctx)
+        throw parseException(s"DataType $dtStr is not supported.", ctx)
     }
   }
 
@@ -607,7 +620,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = YearMonthIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       YearMonthIntervalType(start, end)
     } else {
@@ -622,7 +635,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
       val endStr = ctx.to.getText.toLowerCase(Locale.ROOT)
       val end = DayTimeIntervalType.stringToField(endStr)
       if (end <= start) {
-        throw new ParseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
+        throw parseException(s"Intervals FROM $startStr TO $endStr are not 
supported.", ctx)
       }
       DayTimeIntervalType(start, end)
     } else {
@@ -890,7 +903,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         case ref: FieldReference =>
           ref
         case nonRef =>
-          throw new ParseException(s"Expected a column reference for transform 
$name: $nonRef.describe", ctx)
+          throw parseException(s"Expected a column reference for transform 
$name: ${nonRef.describe}", ctx)
       }
     }
 
@@ -899,7 +912,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
                                  arguments: Seq[V2Expression]): FieldReference 
= {
       lazy val name: String = ctx.identifier.getText
       if (arguments.size > 1) {
-        throw new ParseException(s"Too many arguments for transform $name", 
ctx)
+        throw parseException(s"Too many arguments for transform $name", ctx)
       } else {
         getFieldReference(ctx, arguments.head)
       }
@@ -922,7 +935,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
               case LiteralValue(longValue, LongType) =>
                 longValue.asInstanceOf[Long].toInt
               case lit =>
-                throw new ParseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
+                throw parseException(s"Invalid number of buckets: 
${lit.describe}", applyCtx)
             }
 
             val fields = arguments.tail.map(arg => getFieldReference(applyCtx, 
arg))
@@ -960,7 +973,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
         .map(typedVisit[Literal])
         .map(lit => LiteralValue(lit.value, lit.dataType))
       reference.orElse(literal)
-        .getOrElse(throw new ParseException("Invalid transform argument", ctx))
+        .getOrElse(throw parseException("Invalid transform argument", ctx))
     }
   }
 
@@ -970,13 +983,13 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     val legacyOn = conf.getConf(SQLConf.LEGACY_PROPERTY_NON_RESERVED)
     properties.filter {
       case (PROP_PROVIDER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_PROVIDER is a reserved table 
property, please use the USING clause to specify it.", ctx)
+        throw parseException(s"$PROP_PROVIDER is a reserved table property, 
please use the USING clause to specify it.", ctx)
       case (PROP_PROVIDER, _) => false
       case (PROP_LOCATION, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_LOCATION is a reserved table 
property, please use the LOCATION clause to specify it.", ctx)
+        throw parseException(s"$PROP_LOCATION is a reserved table property, 
please use the LOCATION clause to specify it.", ctx)
       case (PROP_LOCATION, _) => false
       case (PROP_OWNER, _) if !legacyOn =>
-        throw new ParseException(s"$PROP_OWNER is a reserved table property, 
it will be set to the current user.", ctx)
+        throw parseException(s"$PROP_OWNER is a reserved table property, it 
will be set to the current user.", ctx)
       case (PROP_OWNER, _) => false
       case _ => true
     }
@@ -989,7 +1002,7 @@ class HoodieSpark4_2ExtendedSqlAstBuilder(conf: SQLConf, 
delegate: ParserInterfa
     var path = location
     val filtered = cleanTableProperties(ctx, options).filter {
       case (k, v) if k.equalsIgnoreCase("path") && path.nonEmpty =>
-        throw new ParseException(s"Duplicated table paths found: '${path.get}' 
and '$v'. LOCATION" +
+        throw parseException(s"Duplicated table paths found: '${path.get}' and 
'$v'. LOCATION" +
           s" and the case insensitive key 'path' in OPTIONS are all used to 
indicate the custom" +
           s" table path, you can only specify one of them.", ctx)
       case (k, v) if k.equalsIgnoreCase("path") =>

Reply via email to