Skip to content

Commit 1334bce

Browse files
committed
fix(planner): strip implicit casts before matching filter expressions
DataFusion wraps column references in Cast/TryCast/Alias nodes when the query involves type coercion (e.g. comparing an integer column to a float literal). The filter extraction pass was matching on bare Expr::Column, so cast-wrapped columns fell through to the default no-op branch, silently dropping the predicate. Add strip_cast() to recursively unwrap Cast, TryCast, and Alias nodes before pattern matching in all filter extraction paths: binary ops, LIKE, IS NULL / IS NOT NULL, BETWEEN, and IN list. Also remove the range-scan planning path for document collections. That path required a secondary index and silently produced no results when none existed; the data plane now handles this with a full-scan fallback, making the control plane logic simpler and more correct.
1 parent 99c066f commit 1334bce

3 files changed

Lines changed: 18 additions & 16 deletions

File tree

nodedb/src/control/planner/converter.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ impl PlanConverter {
317317
}]);
318318
}
319319
// Document (schemaless/strict) or unknown: try
320-
// point-get, range-scan, then full document scan.
320+
// point-get, then full document scan with filters.
321321
Some(CollectionType::Document(_)) | None => {
322322
if let Some(task) = self.try_point_get(
323323
&collection,
@@ -328,15 +328,6 @@ impl PlanConverter {
328328
return Ok(vec![task]);
329329
}
330330

331-
if let Some(task) = try_range_scan_from_predicate(
332-
&collection,
333-
&filter.predicate,
334-
tenant_id,
335-
vshard,
336-
) {
337-
return Ok(vec![task]);
338-
}
339-
340331
let filter_bytes = serialize_predicate_filters(&filter.predicate)?;
341332
let limit = scan.fetch.unwrap_or(1000);
342333

nodedb/src/control/planner/converter_helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ impl PlanConverter {
106106
}
107107

108108
// Index-ordered scan optimization (single ASC field with LIMIT).
109+
// Falls back to full scan + sort in the Data Plane if no index exists.
109110
if sort.expr.len() == 1
110111
&& sort.expr[0].asc
111112
&& let Expr::Column(col) = &sort.expr[0].expr

nodedb/src/control/planner/extract/filter.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ use crate::bridge::scan_filter::ScanFilter;
99

1010
use super::super::expr_convert::expr_to_json_value;
1111

12+
/// Strip Cast/TryCast/Alias wrappers to get to the core expression.
13+
fn strip_cast(expr: &Expr) -> &Expr {
14+
match expr {
15+
Expr::Cast(c) => strip_cast(&c.expr),
16+
Expr::TryCast(c) => strip_cast(&c.expr),
17+
Expr::Alias(a) => strip_cast(&a.expr),
18+
other => other,
19+
}
20+
}
21+
1222
/// Convert a DataFusion expression to scan filter predicates.
1323
pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<ScanFilter> {
1424
match expr {
@@ -60,7 +70,7 @@ pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<Scan
6070
_ => return Vec::new(),
6171
};
6272

63-
let (field, value) = match (&*binary.left, &*binary.right) {
73+
let (field, value) = match (strip_cast(&binary.left), strip_cast(&binary.right)) {
6474
(Expr::Column(col), Expr::Literal(lit, meta)) => (
6575
col.name.clone(),
6676
nodedb_types::Value::from(expr_to_json_value(&Expr::Literal(
@@ -86,7 +96,7 @@ pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<Scan
8696
}]
8797
}
8898
Expr::Like(like) => {
89-
if let Expr::Column(col) = &*like.expr {
99+
if let Expr::Column(col) = strip_cast(&like.expr) {
90100
let pattern = nodedb_types::Value::from(expr_to_json_value(&like.pattern));
91101
let op = if like.case_insensitive {
92102
if like.negated { "not_ilike" } else { "ilike" }
@@ -106,7 +116,7 @@ pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<Scan
106116
}
107117
}
108118
Expr::IsNull(inner) => {
109-
if let Expr::Column(col) = inner.as_ref() {
119+
if let Expr::Column(col) = strip_cast(inner) {
110120
vec![ScanFilter {
111121
field: col.name.clone(),
112122
op: "is_null".into(),
@@ -118,7 +128,7 @@ pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<Scan
118128
}
119129
}
120130
Expr::IsNotNull(inner) => {
121-
if let Expr::Column(col) = inner.as_ref() {
131+
if let Expr::Column(col) = strip_cast(inner) {
122132
vec![ScanFilter {
123133
field: col.name.clone(),
124134
op: "is_not_null".into(),
@@ -130,7 +140,7 @@ pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<Scan
130140
}
131141
}
132142
Expr::Between(between) => {
133-
if let Expr::Column(col) = &*between.expr {
143+
if let Expr::Column(col) = strip_cast(&between.expr) {
134144
let low = nodedb_types::Value::from(expr_to_json_value(&between.low));
135145
let high = nodedb_types::Value::from(expr_to_json_value(&between.high));
136146
if between.negated {
@@ -177,7 +187,7 @@ pub(in crate::control::planner) fn expr_to_scan_filters(expr: &Expr) -> Vec<Scan
177187
list,
178188
negated,
179189
}) => {
180-
if let Expr::Column(col) = expr.as_ref() {
190+
if let Expr::Column(col) = strip_cast(expr) {
181191
let values: Vec<nodedb_types::Value> = list
182192
.iter()
183193
.map(|e| nodedb_types::Value::from(expr_to_json_value(e)))

0 commit comments

Comments
 (0)