fix(promql): let a too-deep tree fall back instead of failing the query

The depth limit was reported as `DataFusionError::Internal`, the error a
compiler defect gets. The planner only keeps the legacy plan on
`NotImplemented`, so enabling the native route turned an answer into an
error: a statement compiling to more than 64 nodes -- 70 `time()` terms
compile to 139 -- failed outright, where the legacy planner answers it.
Switching a feature flag on must never make a query stop working.

The two rejections now say what they mean. Exceeding the depth limit is
"this statement is beyond what this route handles", which is `NotImplemented`
and makes the planner fall back, from both the constructor and the
deserializer. Every other rejection in `validate()` -- a mismatched binding, a
map over a scalar, a window with the wrong grid -- stays `Internal`, because
those mean the compiler built something inconsistent and reclassifying them
would hide real defects as unsupported queries. A fallback test pins it: 70
`time()` terms must plan exactly as the legacy route does, with the node count
in the message so a future change to the cap explains itself.

`postcard` moves into the external-dependencies block. It is declared with
`default-features = false`, and that block exists precisely for inherited
dependencies whose features are narrowed.

Signed-off-by: discord9 <discord9@163.com>
This commit is contained in:
discord9
2026-11-02 13:34:08 +08:00
parent 52260c762f
commit 8348d3d2d4
8 changed files with 71 additions and 41 deletions
+2 -2
View File
@@ -98,11 +98,11 @@ fn ordinary_encodable_plan_keeps_in_process_encodability_gate() {
let in_process_rewriter = PlanRewriter::new(&plan, true);
assert!(in_process_rewriter.whole_plan_encodable);
assert!(!in_process_rewriter.skip_encodability_gate);
assert!(!in_process_rewriter.skip_in_process_encodability);
let wire_rewriter = PlanRewriter::new(&plan, false);
assert!(wire_rewriter.whole_plan_encodable);
assert!(!wire_rewriter.skip_encodability_gate);
assert!(!wire_rewriter.skip_in_process_encodability);
}
struct MergeScanRemoteDynFilterProducerIdCollector<'a> {
+1 -1
View File
@@ -1012,7 +1012,7 @@ mod tests {
.as_any()
.downcast_ref::<AggregateFinalize>()
.unwrap();
let inputs = finalize.inputs();
let inputs = datafusion_expr::UserDefinedLogicalNodeCore::inputs(finalize);
let [producer] = inputs.as_slice() else {
unreachable!()
};
+19
View File
@@ -2596,6 +2596,25 @@ async fn native_fused_whole_vector_operation_falls_back() {
}
}
#[tokio::test]
async fn native_fused_depth_limit_falls_back_to_legacy_plan() {
let query = std::iter::repeat_n("time()", 70)
.collect::<Vec<_>>()
.join(" + ");
let native = build_native_fused_plan(&query, true).await;
let legacy = build_native_fused_plan(&query, false).await;
let display = native.display_indent().to_string();
assert!(
!display.contains("FusedSeries"),
"70 time() terms compile to 139 native nodes, exceeding the 64-node cap: {display}"
);
assert_eq!(
display,
legacy.display_indent().to_string(),
"139 native nodes exceed the 64-node cap, so planning must preserve the legacy plan"
);
}
#[tokio::test]
async fn native_fused_unsupported_shape_falls_back() {
for query in [