1use std::sync::Arc;
19
20use uqa_core::{
21 IndexStats, PathExpr, PathSegment, Payload, PostingEntry, PostingList, Predicate, Value,
22};
23use uqa_storage::{document_store::Document, StorageBackendError, StorageBackendResult};
24
25use crate::base::{missing_backend, ExecutionContext, Operator, OperatorResult};
26use crate::primitive::FilterOperator;
27
28pub fn parse_path(path: &str) -> PathExpr {
31 path.split('.')
32 .map(|seg| match seg.parse::<usize>() {
33 Ok(n) => PathSegment::Index(n),
34 Err(_) => PathSegment::Key(seg.to_string()),
35 })
36 .collect()
37}
38
39pub fn eval_path(doc: &Document, path: &[PathSegment]) -> Option<Value> {
42 let mut current: Value = match path.first()? {
43 PathSegment::Key(k) => doc.get(k)?.clone(),
44 PathSegment::Index(_) => return None,
45 };
46 for seg in path.iter().skip(1) {
47 current = match (current, seg) {
48 (Value::Map(m), PathSegment::Key(k)) => m.get(k)?.clone(),
49 (Value::List(items), PathSegment::Index(i)) => items.get(*i)?.clone(),
50 (Value::List(items), PathSegment::Key(k)) => {
51 let collected: Vec<Value> = items
54 .into_iter()
55 .filter_map(|v| match v {
56 Value::Map(m) => m.get(k).cloned(),
57 _ => None,
58 })
59 .collect();
60 Value::List(collected)
61 }
62 _ => return None,
63 };
64 }
65 Some(current)
66}
67
68pub fn project_paths(
72 doc: &Document,
73 paths: &[PathExpr],
74) -> std::collections::BTreeMap<String, Value> {
75 let mut out = std::collections::BTreeMap::new();
76 for path in paths {
77 let key = path
78 .iter()
79 .map(|seg| match seg {
80 PathSegment::Key(k) => k.clone(),
81 PathSegment::Index(i) => i.to_string(),
82 })
83 .collect::<Vec<_>>()
84 .join(".");
85 let value = eval_path(doc, path).unwrap_or(Value::Null);
86 out.insert(key, value);
87 }
88 out
89}
90
91pub fn unnest_array(doc: &Document, path: &[PathSegment]) -> StorageBackendResult<Vec<Document>> {
99 let resolved = eval_path(doc, path);
100 let Some(Value::List(items)) = resolved else {
101 return Ok(Vec::new());
102 };
103 let path_key = path
104 .iter()
105 .map(|seg| match seg {
106 PathSegment::Key(k) => k.clone(),
107 PathSegment::Index(i) => i.to_string(),
108 })
109 .collect::<Vec<_>>()
110 .join(".");
111 let unnest_key = format!("{path_key}._unnested");
112 items
113 .into_iter()
114 .enumerate()
115 .map(|(idx, item)| {
116 let mut nested = doc.clone();
117 nested.insert(unnest_key.clone(), item);
118 nested.insert(
119 "_unnest_index".to_string(),
120 Value::Int(i64::try_from(idx).map_err(|_| {
121 StorageBackendError::Other(format!(
122 "unnest index {idx} exceeds the Value::Int range"
123 ))
124 })?),
125 );
126 Ok(nested)
127 })
128 .collect()
129}
130
131pub struct PathFilterOperator {
138 pub path: PathExpr,
139 pub predicate: Predicate,
140 pub source: Option<Arc<dyn Operator>>,
141}
142
143impl PathFilterOperator {
144 pub fn new(path: PathExpr, predicate: Predicate, source: Option<Arc<dyn Operator>>) -> Self {
145 Self {
146 path,
147 predicate,
148 source,
149 }
150 }
151}
152
153impl Operator for PathFilterOperator {
154 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
155 let Some(doc_store) = ctx.document_store.as_ref() else {
156 return Err(missing_backend("document-store", "path filter"));
157 };
158 let candidates: Vec<u64> = match &self.source {
159 Some(src) => src.execute(ctx)?.doc_ids().collect(),
160 None => doc_store.doc_ids()?,
161 };
162 let mut entries: Vec<PostingEntry> = Vec::new();
163 for doc_id in candidates {
164 let doc = doc_store.get(doc_id)?.ok_or_else(|| {
165 StorageBackendError::Other(format!(
166 "path filter candidate {doc_id} is missing from the document store"
167 ))
168 })?;
169 let Some(value) = eval_path(&doc, &self.path) else {
170 if self.predicate.is_null_aware() && self.predicate.evaluate(None) {
171 entries.push(PostingEntry::new(doc_id, Payload::default()));
172 }
173 continue;
174 };
175 let matched = match &value {
176 Value::List(items) => items.iter().any(|v| self.predicate.evaluate(Some(v))),
177 other => self.predicate.evaluate(Some(other)),
178 };
179 if matched {
180 entries.push(PostingEntry::new(doc_id, Payload::default()));
181 }
182 }
183 entries.sort_by_key(|e| e.doc_id);
184 Ok(PostingList::from_sorted_unchecked(entries))
185 }
186
187 fn cost_estimate(&self, stats: &IndexStats) -> f64 {
188 match &self.source {
189 Some(src) => src.cost_estimate(stats),
190 None => stats.total_docs as f64,
191 }
192 }
193}
194
195pub struct PathProjectOperator {
204 pub paths: Vec<PathExpr>,
205 pub source: Arc<dyn Operator>,
206}
207
208impl PathProjectOperator {
209 pub fn new(paths: Vec<PathExpr>, source: Arc<dyn Operator>) -> Self {
210 Self { paths, source }
211 }
212}
213
214impl Operator for PathProjectOperator {
215 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
216 let source_pl = self.source.execute(ctx)?;
217 let Some(doc_store) = ctx.document_store.as_ref() else {
218 return Err(missing_backend("document-store", "path projection"));
219 };
220 let mut entries: Vec<PostingEntry> = Vec::new();
221 for entry in source_pl.entries() {
222 let doc = doc_store.get(entry.doc_id)?.ok_or_else(|| {
223 StorageBackendError::Other(format!(
224 "path projection candidate {} is missing from the document store",
225 entry.doc_id
226 ))
227 })?;
228 let mut fields = entry.payload.fields.clone();
229 for path in &self.paths {
230 if let Some(value) = eval_path(&doc, path) {
231 fields.insert(path_key(path), value);
232 }
233 }
234 entries.push(PostingEntry::new(
235 entry.doc_id,
236 Payload {
237 positions: entry.payload.positions.clone(),
238 score: entry.payload.score,
239 fields,
240 },
241 ));
242 }
243 Ok(PostingList::from_sorted_unchecked(entries))
244 }
245
246 fn cost_estimate(&self, stats: &IndexStats) -> f64 {
247 self.source.cost_estimate(stats)
248 }
249}
250
251fn path_key(path: &[PathSegment]) -> String {
252 let mut parts = Vec::with_capacity(path.len());
253 for seg in path {
254 match seg {
255 PathSegment::Key(k) => parts.push(k.clone()),
256 PathSegment::Index(i) => parts.push(i.to_string()),
257 }
258 }
259 parts.join(".")
260}
261
262#[derive(Debug, Clone, Copy)]
267pub enum AggregationKind {
268 Sum,
269 Avg,
270 Min,
271 Max,
272 Count,
273}
274
275pub struct PathAggregateOperator {
280 pub path: PathExpr,
281 pub agg: AggregationKind,
282 pub source: Option<Arc<dyn Operator>>,
283}
284
285impl PathAggregateOperator {
286 pub fn new(path: PathExpr, agg: AggregationKind, source: Option<Arc<dyn Operator>>) -> Self {
287 Self { path, agg, source }
288 }
289}
290
291impl Operator for PathAggregateOperator {
292 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
293 let Some(doc_store) = ctx.document_store.as_ref() else {
294 return Err(missing_backend("document-store", "path aggregation"));
295 };
296 let candidates: Vec<u64> = match &self.source {
297 Some(src) => src.execute(ctx)?.doc_ids().collect(),
298 None => doc_store.doc_ids()?,
299 };
300 let mut entries: Vec<PostingEntry> = Vec::new();
301 for doc_id in candidates {
302 let doc = doc_store.get(doc_id)?.ok_or_else(|| {
303 StorageBackendError::Other(format!(
304 "path aggregate candidate {doc_id} is missing from the document store"
305 ))
306 })?;
307 let value = eval_path(&doc, &self.path);
308 let mut numeric: Vec<f64> = Vec::new();
309 match value {
310 Some(Value::List(items)) => {
311 for v in items {
312 if let Some(number) = value_as_f64(&v)? {
313 numeric.push(number);
314 }
315 }
316 }
317 Some(other) => {
318 if let Some(number) = value_as_f64(&other)? {
319 numeric.push(number);
320 }
321 }
322 None => {}
323 }
324 let result = aggregate(self.agg, &numeric)?;
325 let mut fields = std::collections::BTreeMap::new();
326 fields.insert(
327 "_path_aggregate_path".into(),
328 Value::Str(path_key(&self.path)),
329 );
330 fields.insert("_path_aggregate".into(), Value::Float(result));
331 entries.push(PostingEntry::new(
332 doc_id,
333 Payload {
334 positions: Vec::new(),
335 score: result,
336 fields,
337 },
338 ));
339 }
340 entries.sort_by_key(|e| e.doc_id);
341 Ok(PostingList::from_sorted_unchecked(entries))
342 }
343
344 fn cost_estimate(&self, stats: &IndexStats) -> f64 {
345 match &self.source {
346 Some(src) => src.cost_estimate(stats),
347 None => stats.total_docs as f64,
348 }
349 }
350}
351
352fn value_as_f64(value: &Value) -> StorageBackendResult<Option<f64>> {
353 let numeric = match value {
354 Value::Null => return Ok(None),
355 Value::Int(number) => *number as f64,
356 Value::Float(number) => *number,
357 Value::Bool(boolean) => {
358 if *boolean {
359 1.0
360 } else {
361 0.0
362 }
363 }
364 _ => {
365 return Err(StorageBackendError::Other(format!(
366 "path aggregation requires numeric values, got {value:?}"
367 )))
368 }
369 };
370 if !numeric.is_finite() {
371 return Err(StorageBackendError::Other(
372 "path aggregation requires finite numeric values".to_string(),
373 ));
374 }
375 Ok(Some(numeric))
376}
377
378fn aggregate(kind: AggregationKind, values: &[f64]) -> StorageBackendResult<f64> {
379 if values.is_empty() {
380 return Ok(0.0);
381 }
382 let result = match kind {
383 AggregationKind::Sum => values.iter().sum(),
384 AggregationKind::Avg => values.iter().sum::<f64>() / values.len() as f64,
385 AggregationKind::Min => values.iter().copied().fold(f64::INFINITY, f64::min),
386 AggregationKind::Max => values.iter().copied().fold(f64::NEG_INFINITY, f64::max),
387 AggregationKind::Count => values.len() as f64,
388 };
389 if !result.is_finite() {
390 return Err(StorageBackendError::Other(
391 "path aggregation overflowed the finite numeric range".to_string(),
392 ));
393 }
394 Ok(result)
395}
396
397pub struct UnifiedFilterOperator {
405 pub field_expr: String,
406 pub predicate: Predicate,
407 pub source: Option<Arc<dyn Operator>>,
408}
409
410impl UnifiedFilterOperator {
411 pub fn new(
412 field_expr: impl Into<String>,
413 predicate: Predicate,
414 source: Option<Arc<dyn Operator>>,
415 ) -> Self {
416 Self {
417 field_expr: field_expr.into(),
418 predicate,
419 source,
420 }
421 }
422}
423
424impl Operator for UnifiedFilterOperator {
425 fn execute(&self, ctx: &ExecutionContext) -> OperatorResult {
426 if self.field_expr.contains('.') {
427 let path = parse_path(&self.field_expr);
428 let inner = PathFilterOperator::new(path, self.predicate.clone(), self.source.clone());
429 inner.execute(ctx)
430 } else {
431 let inner = FilterOperator::new(
432 self.field_expr.clone(),
433 self.predicate.clone(),
434 self.source.clone(),
435 );
436 inner.execute(ctx)
437 }
438 }
439
440 fn cost_estimate(&self, stats: &IndexStats) -> f64 {
441 match &self.source {
442 Some(src) => src.cost_estimate(stats),
443 None => stats.total_docs as f64,
444 }
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451
452 #[test]
453 fn parse_dotted_path() {
454 let p = parse_path("orders.0.amount");
455 assert_eq!(
456 p,
457 vec![
458 PathSegment::Key("orders".into()),
459 PathSegment::Index(0),
460 PathSegment::Key("amount".into()),
461 ]
462 );
463 }
464
465 #[test]
466 fn eval_path_descends_map_then_list_then_key() {
467 let mut doc: Document = std::collections::BTreeMap::new();
468 let mut order = std::collections::BTreeMap::new();
469 order.insert("amount".into(), Value::Int(7));
470 doc.insert("orders".into(), Value::List(vec![Value::Map(order)]));
471 let v = eval_path(&doc, &parse_path("orders.0.amount")).unwrap();
472 assert_eq!(v, Value::Int(7));
473 }
474
475 #[test]
476 fn eval_path_maps_key_over_list_of_maps() {
477 let mut doc: Document = std::collections::BTreeMap::new();
478 let mut o1 = std::collections::BTreeMap::new();
479 o1.insert("amount".into(), Value::Int(7));
480 let mut o2 = std::collections::BTreeMap::new();
481 o2.insert("amount".into(), Value::Int(11));
482 doc.insert(
483 "orders".into(),
484 Value::List(vec![Value::Map(o1), Value::Map(o2)]),
485 );
486 let v = eval_path(&doc, &parse_path("orders.amount")).unwrap();
487 assert_eq!(v, Value::List(vec![Value::Int(7), Value::Int(11)]));
488 }
489}