This is an automated email from the ASF dual-hosted git repository.
MartijnVisser pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new dd2bc4b41ce [FLINK-40540][table-planner] Put the rule's reason first
in the planning error
dd2bc4b41ce is described below
commit dd2bc4b41ce78a5b361d5336b8399b1d5ac0e8a9
Author: Martijn Visser <[email protected]>
AuthorDate: Wed Sep 2 17:17:58 2026 +0200
[FLINK-40540][table-planner] Put the rule's reason first in the planning
error
A rule that rejects a query already explains why, but the message buried
that sentence behind the full logical plan. Put the reason on the header
line and mark the plan with "Plan:" so that tools rewriting the message
can find it. The wrapper type and cause chain stay as they are. A
ValidationException thrown while applying a rule now gets the same
treatment instead of leaking as Calcite's RuntimeException.
Generated-by: Claude Code (Claude Fable 5.1)
---
.../optimize/program/FlinkVolcanoProgram.scala | 63 +++++++++++++++-------
.../plan/batch/sql/agg/GroupWindowTest.scala | 8 ++-
.../optimize/program/FlinkVolcanoProgramTest.scala | 41 +++++++++++++-
.../plan/stream/sql/agg/OverAggregateTest.scala | 4 ++
4 files changed, 94 insertions(+), 22 deletions(-)
diff --git
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgram.scala
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgram.scala
index ac626c94738..32a4c9809fc 100644
---
a/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgram.scala
+++
b/flink-table/flink-table-planner/src/main/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgram.scala
@@ -17,7 +17,8 @@
*/
package org.apache.flink.table.planner.plan.optimize.program
-import org.apache.flink.table.api.TableException
+import org.apache.flink.annotation.VisibleForTesting
+import org.apache.flink.table.api.{TableException, ValidationException}
import org.apache.flink.table.planner.plan.metadata.FlinkRelMdNonCumulativeCost
import org.apache.flink.table.planner.plan.utils.FlinkRelOptUtil
import org.apache.flink.util.Preconditions
@@ -29,6 +30,8 @@ import org.apache.calcite.plan.volcano.VolcanoPlanner
import org.apache.calcite.rel.RelNode
import org.apache.calcite.tools.{Programs, RuleSet}
+import scala.annotation.tailrec
+
/**
* A FlinkRuleSetProgram that runs with
[[org.apache.calcite.plan.volcano.VolcanoPlanner]].
*
@@ -63,32 +66,54 @@ class FlinkVolcanoProgram[OC <: FlinkOptimizeContext]
extends FlinkRuleSetProgra
} catch {
case e: CannotPlanException =>
throw new TableException(
- s"Cannot generate a valid execution plan for the given query: \n\n" +
- s"${FlinkRelOptUtil.toString(root)}\n" +
- s"This exception indicates that the query uses an unsupported SQL
feature.\n" +
- s"Please check the documentation for the set of currently
supported SQL features.",
+ rejectionMessage(
+ "This exception indicates that the query uses an unsupported SQL
feature. " +
+ "Please check the documentation for the set of currently
supported SQL features.",
+ root
+ ),
e)
- case t: TableException =>
- throw new TableException(
- s"Cannot generate a valid execution plan for the given query: \n\n" +
- s"${FlinkRelOptUtil.toString(root)}\n" +
- s"${t.getMessage}\n" +
- s"Please check the documentation for the set of currently
supported SQL features.",
- t)
case a: AssertionError =>
throw new AssertionError(s"Sql optimization: Assertion error:
${a.getMessage}", a)
- case r: RuntimeException if r.getCause.isInstanceOf[TableException] =>
- throw new TableException(
- s"Sql optimization: Cannot generate a valid execution plan for the
given query: \n\n" +
- s"${FlinkRelOptUtil.toString(root)}\n" +
- s"${r.getCause.getMessage}\n" +
- s"Please check the documentation for the set of currently
supported SQL features.",
- r.getCause)
+ case r: RuntimeException =>
+ unwrapRuleException(r) match {
+ case t: TableException =>
+ throw new TableException(rejectionMessage(t.getMessage, root), t)
+ case v: ValidationException =>
+ throw new ValidationException(rejectionMessage(v.getMessage,
root), v)
+ case other => throw other
+ }
} finally {
FlinkRelMdNonCumulativeCost.THREAD_PLANNER.remove()
}
}
+ /**
+ * Puts the reason first so that it is the first line users see, and marks
the plan with a prefix
+ * so that tools rewriting the message can find it.
+ */
+ private def rejectionMessage(reason: String, root: RelNode): String = {
+ s"Cannot generate a valid execution plan for the given query: $reason\n\n"
+
+ s"Plan:\n${FlinkRelOptUtil.toString(root)}"
+ }
+
+ /**
+ * Returns the [[TableException]] or [[ValidationException]] thrown by a
rule, looking through the
+ * plain [[RuntimeException]]s Calcite wraps it in while applying the rule.
Any other exception is
+ * returned unchanged.
+ */
+ @VisibleForTesting
+ private[program] def unwrapRuleException(r: RuntimeException):
RuntimeException = {
+ @tailrec
+ def findRuleException(t: Throwable): Option[RuntimeException] = t match {
+ case e: TableException => Some(e)
+ case e: ValidationException => Some(e)
+ case e if e != null && e.getClass == classOf[RuntimeException] =>
+ findRuleException(e.getCause)
+ case _ => None
+ }
+ findRuleException(r).getOrElse(r)
+ }
+
/** Sets required output traits. */
def setRequiredOutputTraits(relTraits: Array[RelTrait]): Unit = {
Preconditions.checkNotNull(relTraits)
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/batch/sql/agg/GroupWindowTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/batch/sql/agg/GroupWindowTest.scala
index dc38aa53fc3..ca715963f67 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/batch/sql/agg/GroupWindowTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/batch/sql/agg/GroupWindowTest.scala
@@ -290,8 +290,12 @@ class GroupWindowTest(aggStrategy: AggregatePhaseStrategy)
extends TableTestBase
def testNonPartitionedSessionWindow(): Unit = {
val sqlQuery = "SELECT COUNT(*) AS cnt FROM MyTable2 GROUP BY SESSION(ts,
INTERVAL '30' MINUTE)"
assertThatThrownBy(() => util.verifyExecPlan(sqlQuery))
- .hasMessageContaining("Cannot generate a valid execution plan for the
given query")
- .isInstanceOf[TableException]
+ .isInstanceOf(classOf[TableException])
+ // the rule's reason comes first, the plan is marked so that tools can
strip it
+ .hasMessageStartingWith(
+ "Cannot generate a valid execution plan for the given query: Window
SessionGroupWindow(")
+ .hasMessageContaining("is not supported right
now.\n\nPlan:\nFlinkLogicalWindowAggregate(")
+ .hasCauseInstanceOf(classOf[TableException])
}
@TestTemplate
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgramTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgramTest.scala
index 5b987230ca1..c286665384e 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgramTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/optimize/program/FlinkVolcanoProgramTest.scala
@@ -17,11 +17,14 @@
*/
package org.apache.flink.table.planner.plan.optimize.program
+import org.apache.flink.table.api.{TableException, ValidationException}
+
import org.apache.calcite.plan.Convention
+import org.apache.calcite.plan.RelOptPlanner.CannotPlanException
import org.apache.calcite.rel.RelNode
import org.apache.calcite.rel.rules._
import org.apache.calcite.tools.RuleSets
-import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}
import org.junit.jupiter.api.Test
/** Tests for [[FlinkVolcanoProgramTest]]. */
@@ -48,4 +51,40 @@ class FlinkVolcanoProgramTest {
.isInstanceOf(classOf[NullPointerException])
}
+ @Test
+ def testUnwrapRuleExceptionReturnsFlinkExceptionsAsIs(): Unit = {
+ val program = FlinkVolcanoProgramBuilder.newBuilder.build()
+ val tableException = new TableException("rejected by rule")
+ val validationException = new ValidationException("invalid for rule")
+
+
assertThat(program.unwrapRuleException(tableException)).isSameAs(tableException)
+
assertThat(program.unwrapRuleException(validationException)).isSameAs(validationException)
+ }
+
+ @Test
+ def testUnwrapRuleExceptionLooksThroughCalciteWrappers(): Unit = {
+ val program = FlinkVolcanoProgramBuilder.newBuilder.build()
+ val cause = new ValidationException("invalid for rule")
+ val wrappedOnce = new RuntimeException("Error while applying rule", cause)
+ val wrappedTwice = new RuntimeException("Error occurred while applying
rule", wrappedOnce)
+
+ assertThat(program.unwrapRuleException(wrappedOnce)).isSameAs(cause)
+ assertThat(program.unwrapRuleException(wrappedTwice)).isSameAs(cause)
+ }
+
+ @Test
+ def testUnwrapRuleExceptionKeepsOtherExceptionsWrapped(): Unit = {
+ val program = FlinkVolcanoProgramBuilder.newBuilder.build()
+ val wrappedBug = new RuntimeException("Error while applying rule", new
NullPointerException())
+ val wrappedCannotPlan =
+ new RuntimeException("Error while applying rule", new
CannotPlanException("no plan"))
+ val subclassWrapper = new IllegalStateException("wrapped", new
TableException("rejected"))
+ val withoutCause = new RuntimeException("Error while applying rule")
+
+ assertThat(program.unwrapRuleException(wrappedBug)).isSameAs(wrappedBug)
+
assertThat(program.unwrapRuleException(wrappedCannotPlan)).isSameAs(wrappedCannotPlan)
+
assertThat(program.unwrapRuleException(subclassWrapper)).isSameAs(subclassWrapper)
+
assertThat(program.unwrapRuleException(withoutCause)).isSameAs(withoutCause)
+ }
+
}
diff --git
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/OverAggregateTest.scala
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/OverAggregateTest.scala
index c8c7c087802..bb57aba9c4e 100644
---
a/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/OverAggregateTest.scala
+++
b/flink-table/flink-table-planner/src/test/scala/org/apache/flink/table/planner/plan/stream/sql/agg/OverAggregateTest.scala
@@ -66,6 +66,10 @@ class OverAggregateTest extends TableTestBase {
""".stripMargin
assertThatThrownBy(() => util.verifyExecPlan(sqlQuery))
+ // thrown while applying a rule: the exception keeps its type instead of
Calcite's wrapper
+ .isInstanceOf(classOf[ValidationException])
+ .hasMessageStartingWith("Cannot generate a valid execution plan for the
given query: " +
+ "Frame exclusion 'EXCLUDE GROUP' is not supported in over
windows.\n\nPlan:\n")
.hasRootCauseInstanceOf(classOf[ValidationException])
.hasRootCauseMessage("Frame exclusion 'EXCLUDE GROUP' is not supported
in over windows.")
}