Skip to content

Commit f4a9753

Browse files
committed
fix(executor): fall back to full scan when sparse range scan finds no results
Collections without a secondary index on the queried field return empty from the sparse range scan. Previously the handler returned that empty result to the client. Now it falls back to a full collection scan, applies the same sort used by the document sort handler, truncates to the requested limit, and encodes the result — matching the behavior the control plane expects when no index exists. Also fix doc_format's MessagePack detection: zerompk can spuriously succeed on raw JSON bytes by interpreting the leading '{' byte (0x7B) as a fixint. Guard the branch to only accept Object values, so JSON payloads stored in document collections are not misidentified as MessagePack and garbled on read. Widen sort_rows visibility from pub(super) to pub(in crate::data::executor) so the snapshot handler can reuse it without duplication.
1 parent 1334bce commit f4a9753

3 files changed

Lines changed: 61 additions & 10 deletions

File tree

nodedb/src/data/executor/doc_format.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ pub(super) fn json_to_msgpack(bytes: &[u8]) -> Vec<u8> {
7474
}
7575

7676
// Try decoding as nodedb_types::Value (zerompk tagged format) and transcode.
77-
if let Ok(val) = nodedb_types::value_from_msgpack(bytes) {
77+
// Only accept if the result is an Object — zerompk can spuriously "succeed"
78+
// on JSON bytes by interpreting the first byte (e.g. '{' = 0x7B) as a fixint.
79+
if let Ok(val @ nodedb_types::Value::Object(_)) = nodedb_types::value_from_msgpack(bytes) {
7880
let json: serde_json::Value = val.into();
7981
if let Ok(mp) = nodedb_types::json_to_msgpack(&json) {
8082
return mp;

nodedb/src/data/executor/handlers/control/snapshot.rs

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,24 +80,73 @@ impl CoreLoop {
8080
limit: usize,
8181
) -> Response {
8282
debug!(core = self.core_id, %collection, %field, limit, "range scan");
83-
match self
83+
84+
// Try index-backed range scan first.
85+
let results = match self
8486
.sparse
8587
.range_scan(tid, collection, field, lower, upper, limit)
8688
{
87-
Ok(results) => match super::super::super::response_codec::encode(&results) {
88-
Ok(payload) => self.response_with_payload(task, payload),
89+
Ok(r) => r,
90+
Err(e) => {
91+
warn!(core = self.core_id, error = %e, "sparse range scan failed");
92+
return self.response_error(
93+
task,
94+
ErrorCode::Internal {
95+
detail: e.to_string(),
96+
},
97+
);
98+
}
99+
};
100+
101+
// If the index returned nothing, fall back to full scan + sort.
102+
// This handles collections without a secondary index on `field`.
103+
if results.is_empty() {
104+
let scan_result = self.scan_collection(tid, collection, limit.max(1000));
105+
match scan_result {
106+
Ok(mut docs) => {
107+
super::super::document::sort::sort_rows(
108+
&mut docs,
109+
&[(field.to_string(), true)],
110+
);
111+
docs.truncate(limit);
112+
let rows: Vec<_> = docs
113+
.iter()
114+
.map(|(id, val)| {
115+
let data = super::super::super::doc_format::decode_document(val)
116+
.unwrap_or(serde_json::Value::Null);
117+
super::super::super::response_codec::DocumentRow {
118+
id: id.clone(),
119+
data,
120+
}
121+
})
122+
.collect();
123+
match super::super::super::response_codec::encode(&rows) {
124+
Ok(payload) => return self.response_with_payload(task, payload),
125+
Err(e) => {
126+
return self.response_error(
127+
task,
128+
ErrorCode::Internal {
129+
detail: e.to_string(),
130+
},
131+
);
132+
}
133+
}
134+
}
89135
Err(e) => {
90-
warn!(core = self.core_id, error = %e, "range scan serialization failed");
91-
self.response_error(
136+
return self.response_error(
92137
task,
93138
ErrorCode::Internal {
94139
detail: e.to_string(),
95140
},
96-
)
141+
);
97142
}
98-
},
143+
}
144+
}
145+
146+
match super::super::super::response_codec::encode(&results) {
147+
Ok(payload) => self.response_with_payload(task, payload),
99148
Err(e) => {
100-
warn!(core = self.core_id, error = %e, "sparse range scan failed");
149+
warn!(core = self.core_id, error = %e, "range scan serialization failed");
101150
self.response_error(
102151
task,
103152
ErrorCode::Internal {

nodedb/src/data/executor/handlers/document/sort.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ pub(super) fn compare_docs_by_keys_binary(
169169
/// Each entry is `Option<(usize, usize)>` — byte range of the sort key value.
170170
type SortKeyOffsets = Vec<Option<(usize, usize)>>;
171171

172-
pub(super) fn sort_rows(rows: &mut [(String, Vec<u8>)], sort_keys: &[(String, bool)]) {
172+
pub(in crate::data::executor) fn sort_rows(rows: &mut [(String, Vec<u8>)], sort_keys: &[(String, bool)]) {
173173
if sort_keys.is_empty() {
174174
return;
175175
}

0 commit comments

Comments
 (0)