1use rushdb_lmpg::reader::try_access as access;
9use rushdb_lmpg::reconstruct;
10use std::sync::Arc;
11pub type QuerySnapshot = Arc<rushdb_storage::ReadView>;
13use equality::{any_element, is_datetime, value_equals};
14use rushdb_lmpg::{Lmpg, LmpgError};
15use rushdb_types::model::{LogicalType, TypeState};
16use rushdb_types::value::{
17 infer_scalar, scalar_cmp as canonical_scalar_cmp, scalar_equal, Scalar, ID_FIELD,
18};
19use serde_json::{Map, Value as Json};
20use thiserror::Error;
21
22mod aggregate_budget;
23mod aggregate_catalog;
24mod aggregation;
25mod cycle;
26mod equality;
27mod field_types;
28pub use aggregation::MAX_AGGREGATE_BUCKETS;
29pub use aggregation::{
30 aggregate, aggregate_in_view, aggregate_to_rows, aggregate_to_rows_in_view,
31 aggregate_to_rows_in_view_with_budget, aggregate_with_cancel, aggregate_with_cancel_in_view,
32};
33#[cfg(test)]
34use aggregation::{eval_row_expr, exact_f64, minmax};
35use field_types::type_held;
36mod page;
37pub use page::effective_budget;
38mod planned;
39mod planner;
40mod point_fields;
41pub use point_fields::PointFields;
42mod property_predicate;
43pub use hex::hex_decode_16;
44pub use index_access::explain_index;
45pub use planned::{planned_candidates, PlannedCandidates};
46use planner::index_candidates_with_budget;
47pub use planner::{
48 explain_cardinality, explain_range_selectivity, explain_walk, unindexed_walks_total,
49 UNINDEXED_WALK_THRESHOLD,
50};
51pub use property_predicate::{matches_properties, parse_property_predicate};
52pub mod datetime;
53pub mod document;
54mod hex;
55mod index_access;
56mod scan;
57mod stats_cache;
58pub use document::{ParsedQuery, QueryDocumentError};
59
60#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
62pub enum FieldPred {
63 Eq(String, Json),
65 Ne(String, Json),
67 Gt(String, f64),
69 Gte(String, f64),
71 Lt(String, f64),
73 Lte(String, f64),
75 Contains(String, String),
77 StartsWith(String, String),
79 Exists(String, bool),
81 IsNull(String, bool),
83 In(String, Vec<Json>),
85 Nin(String, Vec<Json>),
87 EndsWith(String, String),
89 Id(String),
91 DatetimeComponent {
94 field: String,
96 which: u8,
98 value: i64,
100 },
101 Type(String, String),
103}
104
105#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
107pub enum Expr {
108 And(Vec<Expr>),
110 Or(Vec<Expr>),
112 Xor(Box<Expr>, Box<Expr>),
114 Nor(Vec<Expr>),
116 Not(Box<Expr>),
118 Field(FieldPred),
120 Related(RelatedExpr),
123}
124
125#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
127pub struct RelatedExpr {
128 pub target_labels: Vec<String>,
130 pub alias: Option<String>,
132 pub rel_type: Option<String>,
134 pub direction: Direction,
136 pub hops: Option<(u64, u64)>,
138 pub cycle: bool,
140 pub filter: Option<Box<Expr>>,
143}
144
145#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
147pub struct RelatedSpec {
148 pub alias: Option<String>,
150 pub rel_type: Option<String>,
152 pub direction: Direction,
154 pub target_labels: Vec<String>,
156 pub steps: Vec<TraversalStep>,
162}
163
164#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
166pub struct TraversalStep {
167 pub target_labels: Vec<String>,
169 pub rel_type: Option<String>,
171 pub direction: Direction,
173 pub filter: Option<Expr>,
176}
177
178#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
189pub struct CollectSpec {
190 pub alias: String,
192 pub rel_type: Option<String>,
194 pub direction: Direction,
196 pub target_labels: Vec<String>,
198 pub steps: Vec<TraversalStep>,
202 pub fields: Vec<(String, String)>,
204 pub nested: Vec<CollectSpec>,
206 pub filter: Option<Expr>,
208 pub order_by: Vec<(String, bool)>,
210 pub unique: bool,
212 pub skip: u64,
214 pub limit: Option<u64>,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
220pub enum Direction {
221 Outgoing,
223 Incoming,
225 Both,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
231pub enum TimeUnit {
232 Seconds,
234 Minutes,
236 Hours,
238 Days,
240 Weeks,
242 Months,
244 Quarters,
246 Years,
248}
249
250impl TimeUnit {
251 fn millis(self) -> i64 {
252 match self {
253 TimeUnit::Seconds => 1_000,
254 TimeUnit::Minutes => 60_000,
255 TimeUnit::Hours => 3_600_000,
256 TimeUnit::Days => 86_400_000,
257 TimeUnit::Weeks => 7 * 86_400_000,
258 TimeUnit::Months => 30 * 86_400_000,
259 TimeUnit::Quarters => 90 * 86_400_000,
260 TimeUnit::Years => 365 * 86_400_000,
261 }
262 }
263
264 pub fn from_name(name: &str) -> Result<TimeUnit, crate::QueryDocumentError> {
271 Ok(match name {
272 "second" | "seconds" => TimeUnit::Seconds,
273 "minute" | "minutes" => TimeUnit::Minutes,
274 "hour" | "hours" => TimeUnit::Hours,
275 "day" | "days" => TimeUnit::Days,
276 "week" | "weeks" => TimeUnit::Weeks,
277 "month" | "months" => TimeUnit::Months,
278 "quarter" => TimeUnit::Quarters,
279 "year" | "years" => TimeUnit::Years,
280 other => {
281 return Err(crate::QueryDocumentError::Parse(format!(
282 "unknown time unit `{other}`"
283 )));
284 }
285 })
286 }
287
288 pub(crate) fn unit_str(self) -> &'static str {
289 match self {
290 TimeUnit::Seconds => "seconds",
291 TimeUnit::Minutes => "minutes",
292 TimeUnit::Hours => "hours",
293 TimeUnit::Days => "days",
294 TimeUnit::Weeks => "weeks",
295 TimeUnit::Months => "months",
296 TimeUnit::Quarters => "quarters",
297 TimeUnit::Years => "years",
298 }
299 }
300}
301
302#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
305pub struct TimeBucketSpec {
306 pub field: String,
308 pub unit: TimeUnit,
310 pub size: u64,
312}
313
314pub const MAX_AVG_PRECISION: u32 = 15;
318
319impl TimeBucketSpec {
320 pub fn fixed(field: impl Into<String>, unit: TimeUnit) -> Self {
322 TimeBucketSpec {
323 field: field.into(),
324 unit,
325 size: 1,
326 }
327 }
328
329 pub fn sized(field: impl Into<String>, unit: TimeUnit, size: u64) -> Self {
335 assert!(size > 0, "timeBucket size must be positive");
336 TimeBucketSpec {
337 field: field.into(),
338 unit,
339 size,
340 }
341 }
342
343 #[expect(
344 clippy::cast_possible_truncation,
345 reason = "epoch-ms is a bounded timestamp; the saturating float-to-int cast matches the reference calendar flooring"
346 )]
347 fn floor_millis(&self, epoch_ms: f64) -> i64 {
348 let ms = epoch_ms as i64;
349 match self.unit {
353 TimeUnit::Months => rushdb_types::datetime::start_of_month(ms),
354 TimeUnit::Quarters => rushdb_types::datetime::start_of_quarter(ms),
355 TimeUnit::Years => rushdb_types::datetime::start_of_year(ms),
356 _ => {
357 let size = i64::try_from(self.size).unwrap_or(i64::MAX).max(1);
360 let width = self.unit.millis().saturating_mul(size).max(1);
361 ms.div_euclid(width).saturating_mul(width)
362 }
363 }
364 }
365}
366
367#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
369pub struct Pagination {
370 pub skip: u64,
372 pub limit: Option<u64>,
374}
375
376#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
378pub struct SearchRequest {
379 #[serde(default)]
381 pub labels: Vec<String>,
382 #[serde(default, rename = "where")]
384 pub where_: Option<Expr>,
385 #[serde(default)]
388 pub related: Vec<RelatedSpec>,
389 #[serde(default)]
391 pub collects: Vec<CollectSpec>,
392 #[serde(default)]
394 pub pagination: Pagination,
395 #[serde(default)]
397 pub order_by: Vec<(String, bool)>,
398 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
402 pub force_scan: bool,
403}
404
405#[derive(Debug, Error)]
407pub enum QueryError {
408 #[error("storage error: {0}")]
410 Storage(#[from] rushdb_storage::StorageError),
411 #[error("engine error: {0}")]
413 Engine(#[from] LmpgError),
414 #[error("invalid predicate on `{0}`")]
416 InvalidPredicate(String),
417 #[error("query cancelled")]
419 Cancelled,
420 #[error("resource exhausted: {0}")]
422 ResourceLimit(String),
423 #[error("unindexed walk refused: {0}")]
426 UnindexedWalk(String),
427}
428
429#[derive(Debug, Clone)]
431pub struct Page {
432 pub total_matched: u64,
434 pub records: Vec<Json>,
436}
437
438pub fn find_records(lmpg: &Lmpg, req: &SearchRequest) -> Result<Page, QueryError> {
446 find_records_with_cancel(lmpg, req, &|| false)
447}
448
449pub fn find_records_with_cancel(
458 lmpg: &Lmpg,
459 req: &SearchRequest,
460 is_cancelled: &dyn Fn() -> bool,
461) -> Result<Page, QueryError> {
462 let snap = Arc::new(lmpg.store.read_view()?);
463 find_records_in_view(lmpg, req, is_cancelled, snap)
464}
465
466#[expect(
480 clippy::needless_pass_by_value,
481 reason = "callers hand over a freshly pinned view, matching find_records_in_view"
482)]
483pub fn matching_ordinals(
484 lmpg: &Lmpg,
485 req: &SearchRequest,
486 is_cancelled: &dyn Fn() -> bool,
487 snap: QuerySnapshot,
488) -> Result<Vec<u64>, QueryError> {
489 let is_cancelled: &dyn Fn() -> bool = &cancel_probe(lmpg, is_cancelled);
490 if !snap.belongs_to(&lmpg.store) {
491 return Err(rushdb_storage::StorageError::InvalidRead(
492 "query view belongs to another Store".into(),
493 )
494 .into());
495 }
496 if is_cancelled() {
497 return Err(QueryError::Cancelled);
498 }
499 let mut full = req.clone();
500 full.pagination = Pagination::default();
501 Ok(run_matches(lmpg, &full, is_cancelled, &snap, usize::MAX, None)?.retained)
502}
503
504#[expect(
513 clippy::needless_pass_by_value,
514 reason = "callers hand over a freshly pinned view, matching find_records_in_view"
515)]
516pub fn matching_ordinals_first(
517 lmpg: &Lmpg,
518 req: &SearchRequest,
519 is_cancelled: &dyn Fn() -> bool,
520 snap: QuerySnapshot,
521 keep: usize,
522) -> Result<Vec<u64>, QueryError> {
523 let is_cancelled: &dyn Fn() -> bool = &cancel_probe(lmpg, is_cancelled);
524 if !snap.belongs_to(&lmpg.store) {
525 return Err(rushdb_storage::StorageError::InvalidRead(
526 "query view belongs to another Store".into(),
527 )
528 .into());
529 }
530 if is_cancelled() {
531 return Err(QueryError::Cancelled);
532 }
533 let mut full = req.clone();
534 full.pagination = Pagination::default();
535 Ok(run_matches(lmpg, &full, is_cancelled, &snap, keep, None)?.retained)
536}
537
538struct Matches {
540 retained: Vec<u64>,
542 total: u64,
544}
545
546fn run_matches(
558 lmpg: &Lmpg,
559 req: &SearchRequest,
560 is_cancelled: &dyn Fn() -> bool,
561 snap: &QuerySnapshot,
562 keep: usize,
563 known_total: Option<u64>,
564) -> Result<Matches, QueryError> {
565 check_walk_budget(lmpg, snap, req)?;
566 let mut working = WorkingSet {
567 limit: working_set_budget(lmpg).map(|bytes| u64::try_from(bytes).unwrap_or(u64::MAX)),
568 charged: 0,
569 };
570 let CandidateSource {
571 mut candidates,
572 exact,
573 known,
574 predicate,
575 } = candidate_source(lmpg, req, is_cancelled, snap, known_total, &mut working)?;
576 let ctx = EvalCtx {
577 lmpg,
578 snap: snap.clone(),
579 is_cancelled: Some(is_cancelled),
580 cycles: crate::cycle::CycleGraphs::default(),
581 declared_types: std::cell::RefCell::default(),
582 record_types: std::cell::RefCell::default(),
583 };
584 let mut session = reconstruct::ReconstructSession::new(snap, lmpg.tenant);
585 let order_fields = if req.order_by.is_empty() {
586 None
587 } else {
588 PointFields::resolve(
589 snap,
590 lmpg.tenant,
591 req.order_by.iter().map(|(f, _)| f.as_str()),
592 )?
593 };
594 let predicate_fields = match &predicate {
595 Some(expr) => PointFields::for_predicate(snap, lmpg.tenant, expr)?,
596 None => None,
597 };
598 let mut retained: Vec<u64> = Vec::new();
599 let mut ordered: Vec<OrderRow> = Vec::new();
602 let mut ordered_bytes: Vec<u64> = Vec::new();
603 let mut total: u64 = 0;
604 while let Some(ord) = candidates.next(is_cancelled)? {
605 if is_cancelled() {
606 return Err(QueryError::Cancelled);
607 }
608 if !exact
609 && !is_match(
610 &ctx,
611 req,
612 predicate.as_ref(),
613 predicate_fields.as_ref(),
614 &mut session,
615 &mut working,
616 ord,
617 )?
618 {
619 continue;
620 }
621 total += 1;
622 if req.order_by.is_empty() {
623 if retained.len() < keep {
624 working.charge(8, true)?;
625 retained.push(ord);
626 } else if known.is_some() {
627 break;
628 }
629 continue;
630 }
631 let row = read_row(order_fields.as_ref(), &mut session, snap, lmpg.tenant, ord)?;
632 let bytes =
633 serde_json::to_vec(&row).map_or(0, |v| u64::try_from(v.len()).unwrap_or(u64::MAX));
634 working.charge(bytes, true)?;
635 ordered.push((ord, row));
636 ordered_bytes.push(bytes);
637 if ordered.len() >= keep.saturating_mul(2).max(64) {
638 working.charged = working.charged.saturating_sub(cut_ordered(
639 &mut ordered,
640 &mut ordered_bytes,
641 req,
642 keep,
643 ));
644 }
645 }
646 if !req.order_by.is_empty() {
647 cut_ordered(&mut ordered, &mut ordered_bytes, req, keep);
648 retained = ordered.into_iter().map(|(ord, _)| ord).collect();
649 }
650 Ok(Matches {
651 retained,
652 total: known.unwrap_or(total),
653 })
654}
655
656struct WorkingSet {
658 limit: Option<u64>,
659 charged: u64,
660}
661
662impl WorkingSet {
663 fn charge(&mut self, bytes: u64, retained: bool) -> Result<(), QueryError> {
666 if let Some(limit) = self.limit {
667 if self.charged.saturating_add(bytes) > limit {
668 return Err(QueryError::ResourceLimit(format!(
669 "query working set exceeded {limit} bytes"
670 )));
671 }
672 if retained {
673 self.charged += bytes;
674 }
675 }
676 Ok(())
677 }
678}
679
680fn is_match(
683 ctx: &EvalCtx<'_>,
684 req: &SearchRequest,
685 predicate: Option<&Expr>,
686 fields: Option<&PointFields>,
687 session: &mut reconstruct::ReconstructSession<'_, QuerySnapshot>,
688 working: &mut WorkingSet,
689 ord: u64,
690) -> Result<bool, QueryError> {
691 let (snap, tenant) = (&ctx.snap, ctx.lmpg.tenant);
692 if access::record_by_ord(snap, tenant, ord)?.is_none() {
693 return Ok(false);
694 }
695 if let Some(expr) = predicate {
696 let obj = read_row(fields, session, snap, tenant, ord)?;
697 let bytes =
698 serde_json::to_vec(&obj).map_or(0, |v| u64::try_from(v.len()).unwrap_or(u64::MAX));
699 working.charge(bytes, false)?;
700 if !eval_expr_ctx(ctx, expr, &obj)? {
701 return Ok(false);
702 }
703 }
704 for spec in &req.related {
705 if !related_exists(snap, tenant, ord, spec)? {
706 return Ok(false);
707 }
708 }
709 Ok(true)
710}
711
712fn filtered_row<R: rushdb_lmpg::reader::TryRead>(
717 reader: &R,
718 snap: &QuerySnapshot,
719 tenant: rushdb_types::ids::TenantId,
720 predicate: &Expr,
721 ord: u64,
722) -> Result<Map<String, Json>, QueryError> {
723 let mut session = reconstruct::ReconstructSession::new(reader, tenant);
724 let mut names = std::collections::BTreeSet::new();
725 let record = if point_fields::predicate_fields(predicate, &mut names) {
726 let selected: std::collections::HashSet<String> = names
727 .iter()
728 .flat_map(|name| [name.clone(), normalize(name)])
729 .collect();
730 session.reconstruct_record_selected(ord, &selected)?
731 } else {
732 session.reconstruct_record(ord)?
733 };
734 let _ = snap;
735 Ok(match record {
736 Some(Json::Object(map)) => map,
737 _ => Map::new(),
738 })
739}
740
741fn read_row(
743 fields: Option<&PointFields>,
744 session: &mut reconstruct::ReconstructSession<'_, QuerySnapshot>,
745 snap: &QuerySnapshot,
746 tenant: rushdb_types::ids::TenantId,
747 ord: u64,
748) -> Result<Map<String, Json>, QueryError> {
749 match fields {
750 Some(fields) => fields.row(snap, tenant, ord),
751 None => Ok(session
752 .reconstruct_record(ord)?
753 .and_then(|record| match record {
754 Json::Object(map) => Some(map),
755 _ => None,
756 })
757 .unwrap_or_default()),
758 }
759}
760
761struct CandidateSource<'v> {
763 candidates: scan::Candidates<'v>,
764 exact: bool,
767 known: Option<u64>,
769 predicate: Option<Expr>,
772}
773
774fn candidate_source<'v>(
777 lmpg: &Lmpg,
778 req: &SearchRequest,
779 is_cancelled: &dyn Fn() -> bool,
780 snap: &'v QuerySnapshot,
781 known_total: Option<u64>,
782 working: &mut WorkingSet,
783) -> Result<CandidateSource<'v>, QueryError> {
784 if let Some(plan) =
785 index_candidates_with_budget(lmpg, snap, req, working_set_budget(lmpg), is_cancelled)?
786 {
787 working.charge(
788 u64::try_from(plan.ords.serialized_size()).unwrap_or(u64::MAX),
789 true,
790 )?;
791 let known = known_total
792 .or(plan.known_total)
793 .or_else(|| plan.exact.then(|| plan.ords.len()));
794 return Ok(CandidateSource {
795 candidates: scan::Candidates::from_set(plan.ords),
796 exact: plan.exact,
797 known,
798 predicate: plan.residual,
799 });
800 }
801 let exact = req.where_.is_none() && req.related.is_empty();
802 let known = match known_total {
803 Some(total) => Some(total),
804 None if exact => Some(walk_total(lmpg, snap, &req.labels)?),
805 None => None,
806 };
807 Ok(CandidateSource {
808 candidates: scan::Candidates::walk(snap, lmpg.tenant, &req.labels)?,
809 exact,
810 known,
811 predicate: req.where_.clone(),
812 })
813}
814
815type OrderRow = (u64, Map<String, Json>);
817
818fn cut_ordered(
821 ordered: &mut Vec<OrderRow>,
822 bytes: &mut Vec<u64>,
823 req: &SearchRequest,
824 keep: usize,
825) -> u64 {
826 let mut rows: Vec<(OrderRow, u64)> = ordered.drain(..).zip(bytes.drain(..)).collect();
827 sort_ordered_rows(&mut rows, req);
831 let freed = rows.iter().skip(keep).map(|(_, b)| *b).sum();
832 rows.truncate(keep);
833 for (row, b) in rows {
834 ordered.push(row);
835 bytes.push(b);
836 }
837 freed
838}
839
840fn walk_total(lmpg: &Lmpg, snap: &QuerySnapshot, labels: &[String]) -> Result<u64, QueryError> {
842 if labels.is_empty() {
843 return Ok(rushdb_lmpg::derived::tenant_usage(snap.as_ref(), lmpg.tenant)?.records);
844 }
845 let distinct: std::collections::BTreeSet<&String> = labels.iter().collect();
846 let mut total = 0u64;
847 for label in distinct {
848 total = total.saturating_add(rushdb_lmpg::derived::label_count(
849 snap.as_ref(),
850 lmpg.tenant,
851 label,
852 )?);
853 }
854 Ok(total)
855}
856
857pub fn find_records_in_view(
867 lmpg: &Lmpg,
868 req: &SearchRequest,
869 is_cancelled: &dyn Fn() -> bool,
870 snap: QuerySnapshot,
871) -> Result<Page, QueryError> {
872 find_records_in_view_with_budget(lmpg, req, is_cancelled, snap, None)
873}
874
875fn check_walk_budget(
881 lmpg: &Lmpg,
882 snap: &QuerySnapshot,
883 req: &SearchRequest,
884) -> Result<(), QueryError> {
885 let (Some(limit), Some(expr)) = (lmpg.limits().unindexed_walk_max_owners(), &req.where_) else {
886 return Ok(());
887 };
888 if explain_index(snap.as_ref(), lmpg.tenant, &req.labels, expr)?.is_some() {
889 return Ok(());
890 }
891 let walk = explain_walk(snap.as_ref(), lmpg.tenant, &req.labels, expr)?;
892 let (property, owners, labels) = if let Some(walk) = walk {
893 walk
894 } else {
895 let owners = if req.labels.is_empty() {
897 rushdb_lmpg::derived::tenant_usage(snap.as_ref(), lmpg.tenant)?.records
898 } else {
899 let mut owners = 0u64;
900 for label in &req.labels {
901 owners = owners.saturating_add(rushdb_lmpg::derived::label_count(
902 snap.as_ref(),
903 lmpg.tenant,
904 label,
905 )?);
906 }
907 owners
908 };
909 (
910 "(no indexable condition)".to_owned(),
911 owners,
912 req.labels.clone(),
913 )
914 };
915 if owners <= limit {
916 return Ok(());
917 }
918 Err(QueryError::UnindexedWalk(format!(
919 "the filter on `{property}` would check {owners} records of [{}], over \
920 the unindexed-walk limit ({limit}); declare an ordered index on the property",
921 labels.join(", ")
922 )))
923}
924
925fn working_set_budget(lmpg: &Lmpg) -> Option<usize> {
932 lmpg.limits()
933 .query_memory_max_bytes()
934 .map(|bytes| usize::try_from(bytes).unwrap_or(usize::MAX))
935}
936
937pub(crate) fn cancel_probe<'a>(
940 lmpg: &Lmpg,
941 is_cancelled: &'a dyn Fn() -> bool,
942) -> impl Fn() -> bool + 'a {
943 let limits = lmpg.limits();
944 move || is_cancelled() || limits.expired()
945}
946
947#[allow(clippy::needless_pass_by_value)]
955pub fn find_records_in_view_with_budget(
956 lmpg: &Lmpg,
957 req: &SearchRequest,
958 is_cancelled: &dyn Fn() -> bool,
959 snap: QuerySnapshot,
960 work_budget: Option<usize>,
961) -> Result<Page, QueryError> {
962 find_page(lmpg, req, is_cancelled, &snap, work_budget, None)
963}
964
965pub fn find_records_streaming(
973 lmpg: &Lmpg,
974 req: &SearchRequest,
975 is_cancelled: &dyn Fn() -> bool,
976 snap: QuerySnapshot,
977) -> Result<Page, QueryError> {
978 find_records_streaming_with_total(lmpg, req, is_cancelled, snap, None)
979}
980
981#[expect(
992 clippy::needless_pass_by_value,
993 reason = "the snapshot is accepted by value as part of the public API contract so callers can hand over a freshly pinned view"
994)]
995pub fn find_records_streaming_with_total(
996 lmpg: &Lmpg,
997 req: &SearchRequest,
998 is_cancelled: &dyn Fn() -> bool,
999 snap: QuerySnapshot,
1000 known_total: Option<u64>,
1001) -> Result<Page, QueryError> {
1002 find_page(lmpg, req, is_cancelled, &snap, None, known_total)
1003}
1004
1005fn find_page(
1008 lmpg: &Lmpg,
1009 req: &SearchRequest,
1010 is_cancelled: &dyn Fn() -> bool,
1011 snap: &QuerySnapshot,
1012 work_budget: Option<usize>,
1013 known_total: Option<u64>,
1014) -> Result<Page, QueryError> {
1015 let is_cancelled: &dyn Fn() -> bool = &cancel_probe(lmpg, is_cancelled);
1016 if !snap.belongs_to(&lmpg.store) {
1017 return Err(rushdb_storage::StorageError::InvalidRead(
1018 "query view belongs to another Store".into(),
1019 )
1020 .into());
1021 }
1022 if is_cancelled() {
1023 return Err(QueryError::Cancelled);
1024 }
1025 let skip = usize::try_from(req.pagination.skip).unwrap_or(usize::MAX);
1026 let limit = usize::try_from(req.pagination.limit.unwrap_or(u64::MAX)).unwrap_or(usize::MAX);
1027 let matches = run_matches(
1028 lmpg,
1029 req,
1030 is_cancelled,
1031 snap,
1032 skip.saturating_add(limit),
1033 known_total,
1034 )?;
1035 let page_roots: Vec<u64> = matches
1036 .retained
1037 .into_iter()
1038 .skip(skip)
1039 .take(limit)
1040 .collect();
1041 let budget = effective_budget(
1042 work_budget,
1043 lmpg.limits().query_memory_max_bytes().unwrap_or(0),
1044 );
1045 let records =
1046 page::reconstruct_page_with_budget(lmpg, req, is_cancelled, snap, page_roots, budget)?;
1047 Ok(Page {
1048 total_matched: matches.total,
1049 records,
1050 })
1051}
1052
1053fn sort_ordered_rows<T>(sortable: &mut [(OrderRow, T)], req: &SearchRequest) {
1056 for (field, desc) in req.order_by.iter().rev() {
1057 let field = normalize(field);
1058 sortable.sort_by(|((_, a), _), ((_, b), _)| {
1059 let va = a
1060 .iter()
1061 .find(|(k, _)| normalize(k) == field)
1062 .map(|(_, v)| v);
1063 let vb = b
1064 .iter()
1065 .find(|(k, _)| normalize(k) == field)
1066 .map(|(_, v)| v);
1067 let ord = match (va, vb) {
1068 (Some(x), Some(y)) => json_value_cmp(x, y),
1069 (None, Some(_)) => std::cmp::Ordering::Less,
1070 (Some(_), None) => std::cmp::Ordering::Greater,
1071 (None, None) => std::cmp::Ordering::Equal,
1072 };
1073 if *desc {
1074 ord.reverse()
1075 } else {
1076 ord
1077 }
1078 });
1079 }
1080}
1081
1082type RelatedItem = (u64, Map<String, Json>, Map<String, Json>);
1084
1085fn collect_neighbors(
1086 lmpg: &Lmpg,
1087 snap: &QuerySnapshot,
1088 root: u64,
1089 spec: &CollectSpec,
1090 budget: Option<usize>,
1091 is_cancelled: &dyn Fn() -> bool,
1092) -> Result<Vec<u64>, QueryError> {
1093 Ok(if spec.steps.is_empty() {
1094 let edges: Vec<(String, u64, u64)> = match spec.direction {
1095 Direction::Outgoing => {
1096 access::adj_out(snap, lmpg.tenant, root, spec.rel_type.as_deref())?
1097 }
1098 Direction::Incoming => {
1099 access::adj_in(snap, lmpg.tenant, root, spec.rel_type.as_deref())?
1100 }
1101 Direction::Both => {
1102 let mut all = access::adj_out(snap, lmpg.tenant, root, spec.rel_type.as_deref())?;
1103 all.extend(access::adj_in(
1104 snap,
1105 lmpg.tenant,
1106 root,
1107 spec.rel_type.as_deref(),
1108 )?);
1109 all
1110 }
1111 };
1112 edges.into_iter().map(|(_, other, _)| other).collect()
1113 } else {
1114 traverse_alias(
1115 lmpg,
1116 snap,
1117 lmpg.tenant,
1118 root,
1119 &spec.steps,
1120 budget,
1121 is_cancelled,
1122 )?
1123 })
1124}
1125
1126fn order_collected_items(items: &mut Vec<RelatedItem>, spec: &CollectSpec) {
1127 if spec.unique {
1129 items.sort_by_key(|(o, _, _)| *o);
1130 items.dedup_by(|a, b| a.1.get(ID_FIELD) == b.1.get(ID_FIELD));
1131 } else {
1132 items.sort_by_key(|(o, _, _)| *o);
1133 }
1134 if !spec.order_by.is_empty() {
1136 items.sort_by(|a, b| {
1137 for (field, desc) in &spec.order_by {
1138 let av = raw_field(&a.2, field).or_else(|| raw_field(&a.1, field));
1139 let bv = raw_field(&b.2, field).or_else(|| raw_field(&b.1, field));
1140 let ord = scalar_cmp(av, bv).unwrap_or(std::cmp::Ordering::Equal);
1141 if ord != std::cmp::Ordering::Equal {
1142 return if *desc { ord.reverse() } else { ord };
1143 }
1144 }
1145 std::cmp::Ordering::Equal
1146 });
1147 }
1148}
1149
1150fn attach_collects_with_budget(
1151 lmpg: &Lmpg,
1152 snap: &QuerySnapshot,
1153 root: u64,
1154 mut record: Json,
1155 collects: &[CollectSpec],
1156 budget: Option<usize>,
1157 is_cancelled: &dyn Fn() -> bool,
1158) -> Result<Json, QueryError> {
1159 if collects.is_empty() {
1160 return Ok(record);
1161 }
1162 let mut retained = aggregate_budget::value_bytes(&record);
1163 aggregate_budget::check(budget, retained)?;
1164 let ctx = EvalCtx {
1165 lmpg,
1166 snap: snap.clone(),
1167 is_cancelled: None,
1168 cycles: crate::cycle::CycleGraphs::default(),
1169 declared_types: std::cell::RefCell::default(),
1170 record_types: std::cell::RefCell::default(),
1171 };
1172 if let Some(obj) = record.as_object_mut() {
1173 for spec in collects {
1174 if is_cancelled() {
1178 return Err(QueryError::Cancelled);
1179 }
1180 let neighbors = collect_neighbors(
1181 lmpg,
1182 snap,
1183 root,
1184 spec,
1185 budget.map(|limit| limit.saturating_sub(retained)),
1186 is_cancelled,
1187 )?;
1188 let base_bytes = retained;
1190 retained = retained.saturating_add(neighbors.capacity().saturating_mul(8));
1191 aggregate_budget::check(budget, retained)?;
1192 let selected = point_fields::collect_selection(spec);
1195 let mut items: Vec<RelatedItem> = Vec::new();
1196 for other in neighbors {
1197 if is_cancelled() {
1198 return Err(QueryError::Cancelled);
1199 }
1200 if !has_target_label(snap, lmpg.tenant, other, &spec.target_labels)? {
1201 continue;
1202 }
1203 let Some(child) = aggregate_budget::collected_child(
1204 snap,
1205 lmpg.tenant,
1206 other,
1207 selected.as_ref(),
1208 budget.map(|limit| limit.saturating_sub(retained)),
1209 )?
1210 else {
1211 continue;
1212 };
1213 if let Some(filter) = &spec.filter {
1215 if !eval_expr_ctx(&ctx, filter, &child)? {
1216 continue;
1217 }
1218 }
1219 retained = retained
1220 .saturating_add(aggregate_budget::object_bytes(&child).saturating_mul(3));
1221 aggregate_budget::check(budget, retained)?;
1222 let projected = match project_fields_map(Json::Object(child.clone()), &spec.fields)
1223 {
1224 Json::Object(p) => p,
1225 _ => child.clone(),
1226 };
1227 items.push((other, child, projected));
1228 }
1229 order_collected_items(&mut items, spec);
1230 let start = usize::try_from(spec.skip)
1232 .unwrap_or(usize::MAX)
1233 .min(items.len());
1234 let end = spec.limit.map_or(items.len(), |l| {
1235 start
1236 .saturating_add(usize::try_from(l).unwrap_or(usize::MAX))
1237 .min(items.len())
1238 });
1239 let mut projected = Vec::new();
1240 for (other, raw, child) in items.into_iter().take(end).skip(start) {
1241 retained =
1242 retained.saturating_sub(aggregate_budget::object_bytes(&raw).saturating_mul(3));
1243 let child = attach_collects_with_budget(
1244 lmpg,
1245 snap,
1246 other,
1247 Json::Object(child),
1248 &spec.nested,
1249 budget.map(|limit| limit.saturating_sub(retained)),
1250 is_cancelled,
1251 )?;
1252 retained = retained.saturating_add(aggregate_budget::value_bytes(&child));
1253 aggregate_budget::check(budget, retained)?;
1254 projected.push(child);
1255 }
1256 let collected = Json::Array(projected);
1257 retained = base_bytes
1258 .saturating_add(spec.alias.len())
1259 .saturating_add(aggregate_budget::value_bytes(&collected));
1260 aggregate_budget::check(budget, retained)?;
1261 obj.insert(spec.alias.clone(), collected);
1262 }
1263 }
1264 Ok(record)
1265}
1266
1267fn has_target_label(
1272 snap: &QuerySnapshot,
1273 tenant: rushdb_types::ids::TenantId,
1274 ordinal: u64,
1275 target_labels: &[String],
1276) -> Result<bool, QueryError> {
1277 if target_labels.is_empty() {
1278 return Ok(true);
1279 }
1280 Ok(access::record_by_ord(snap, tenant, ordinal)?
1281 .is_some_and(|header| target_labels.contains(&header.label)))
1282}
1283
1284fn project_fields_map(value: Json, fields: &[(String, String)]) -> Json {
1285 if fields.is_empty() {
1286 return value;
1287 }
1288 let mut out = Map::new();
1289 if let Json::Object(obj) = value {
1290 for (out_key, source) in fields {
1291 if let Some(v) = resolve_field(&obj, source) {
1292 out.insert(out_key.clone(), v);
1293 }
1294 }
1295 }
1296 Json::Object(out)
1297}
1298
1299fn scalar_cmp(a: Option<&Json>, b: Option<&Json>) -> Option<std::cmp::Ordering> {
1302 let (Some(a), Some(b)) = (a, b) else {
1303 return None;
1304 };
1305 match (a, b) {
1306 (Json::Number(_), Json::Number(_)) => {
1307 let left = infer_scalar(a).map(|(_, scalar)| scalar);
1308 let right = infer_scalar(b).map(|(_, scalar)| scalar);
1309 left.as_ref()
1310 .zip(right.as_ref())
1311 .and_then(|(left, right)| canonical_scalar_cmp(left, right))
1312 }
1313 (Json::String(x), Json::String(y)) => Some(x.cmp(y)),
1314 _ => None,
1315 }
1316}
1317
1318fn related_exists(
1319 snap: &QuerySnapshot,
1320 tenant: rushdb_types::ids::TenantId,
1321 ord: u64,
1322 spec: &RelatedSpec,
1323) -> Result<bool, QueryError> {
1324 let edges: Vec<(String, u64, u64)> = match spec.direction {
1325 Direction::Outgoing => access::adj_out(snap, tenant, ord, spec.rel_type.as_deref())?,
1326 Direction::Incoming => access::adj_in(snap, tenant, ord, spec.rel_type.as_deref())?,
1327 Direction::Both => {
1328 let mut all = access::adj_out(snap, tenant, ord, spec.rel_type.as_deref())?;
1329 all.extend(access::adj_in(snap, tenant, ord, spec.rel_type.as_deref())?);
1330 all
1331 }
1332 };
1333 if spec.target_labels.is_empty() {
1334 return Ok(!edges.is_empty());
1335 }
1336 for (_, other_ord, _) in edges {
1337 if access::record_by_ord(snap, tenant, other_ord)?
1338 .is_some_and(|h| spec.target_labels.contains(&h.label))
1339 {
1340 return Ok(true);
1341 }
1342 }
1343 Ok(false)
1344}
1345
1346fn count_related(
1352 lmpg: &Lmpg,
1353 snap: &QuerySnapshot,
1354 tenant: rushdb_types::ids::TenantId,
1355 ord: u64,
1356 specs: &[RelatedSpec],
1357 alias: &str,
1358 budget: Option<usize>,
1359) -> Result<Option<Vec<u64>>, QueryError> {
1360 let Some(spec) = specs.iter().find(|s| {
1361 s.alias
1362 .as_deref()
1363 .is_some_and(|a| a.trim_start_matches('$') == alias)
1364 }) else {
1365 return Ok(None);
1366 };
1367 Ok(Some(traverse_alias(
1368 lmpg,
1369 snap,
1370 tenant,
1371 ord,
1372 &spec.steps,
1373 budget,
1374 &|| false,
1375 )?))
1376}
1377
1378fn traverse_alias(
1382 lmpg: &Lmpg,
1383 snap: &QuerySnapshot,
1384 tenant: rushdb_types::ids::TenantId,
1385 ord: u64,
1386 steps: &[TraversalStep],
1387 budget: Option<usize>,
1388 is_cancelled: &dyn Fn() -> bool,
1389) -> Result<Vec<u64>, QueryError> {
1390 if steps.is_empty() {
1391 return Ok(Vec::new());
1392 }
1393 let ctx = EvalCtx {
1394 lmpg,
1395 snap: snap.clone(),
1396 is_cancelled: Some(is_cancelled),
1397 cycles: crate::cycle::CycleGraphs::default(),
1398 declared_types: std::cell::RefCell::default(),
1399 record_types: std::cell::RefCell::default(),
1400 };
1401 let mut cur = vec![ord];
1402 for step in steps {
1403 if is_cancelled() {
1404 return Err(QueryError::Cancelled);
1405 }
1406 let mut next: Vec<u64> = Vec::new();
1407 for o in &cur {
1408 if is_cancelled() {
1409 return Err(QueryError::Cancelled);
1410 }
1411 let edges: Vec<(String, u64, u64)> = match step.direction {
1412 Direction::Outgoing => access::adj_out(snap, tenant, *o, step.rel_type.as_deref())?,
1413 Direction::Incoming => access::adj_in(snap, tenant, *o, step.rel_type.as_deref())?,
1414 Direction::Both => {
1415 let mut all = access::adj_out(snap, tenant, *o, step.rel_type.as_deref())?;
1416 all.extend(access::adj_in(snap, tenant, *o, step.rel_type.as_deref())?);
1417 all
1418 }
1419 };
1420 for (_, other, _) in edges {
1421 if !step.target_labels.is_empty()
1422 && !access::record_by_ord(snap, tenant, other)?
1423 .is_some_and(|h| step.target_labels.contains(&h.label))
1424 {
1425 continue;
1426 }
1427 if let Some(pred) = &step.filter {
1428 let reader = aggregate_budget::RecordReader::new(
1429 snap,
1430 budget.map(|limit| {
1431 limit.saturating_sub(
1432 cur.len().saturating_add(next.len()).saturating_mul(32),
1433 )
1434 }),
1435 );
1436 let obj = filtered_row(&reader, snap, tenant, pred, other)?;
1437 if !eval_expr_ctx(&ctx, pred, &obj)? {
1438 continue;
1439 }
1440 }
1441 aggregate_budget::check(
1442 budget,
1443 cur.len()
1444 .saturating_add(next.len())
1445 .saturating_add(1)
1446 .saturating_mul(32),
1447 )?;
1448 next.push(other);
1449 }
1450 }
1451 next.sort_unstable();
1452 next.dedup();
1453 cur = next;
1454 }
1455 Ok(cur)
1456}
1457
1458fn related_specs_from_where(expr: Option<&Expr>) -> Vec<RelatedSpec> {
1462 fn split_level(filter: Option<&Expr>, children: &mut Vec<Expr>) -> Option<Expr> {
1465 let f = filter?;
1466 match f {
1467 Expr::And(items) => {
1468 let mut preds: Vec<Expr> = Vec::new();
1469 for item in items {
1470 match item {
1471 Expr::Related(_) => children.push(item.clone()),
1472 other => preds.push(other.clone()),
1473 }
1474 }
1475 match preds.len() {
1476 0 => None,
1477 1 => Some(preds.remove(0)),
1478 _ => Some(Expr::And(preds)),
1479 }
1480 }
1481 Expr::Related(_) => {
1482 children.push(f.clone());
1483 None
1484 }
1485 other => Some(other.clone()),
1486 }
1487 }
1488
1489 fn walk(expr: &Expr, path: &mut Vec<TraversalStep>, out: &mut Vec<RelatedSpec>) {
1490 match expr {
1491 Expr::And(items) | Expr::Or(items) | Expr::Nor(items) => {
1492 for e in items {
1493 walk(e, path, out);
1494 }
1495 }
1496 Expr::Xor(a, b) => {
1497 walk(a, path, out);
1498 walk(b, path, out);
1499 }
1500 Expr::Not(b) => {
1501 walk(b, path, out);
1502 }
1503 Expr::Related(rel) => {
1504 let mut children: Vec<Expr> = Vec::new();
1505 let filter = split_level(rel.filter.as_deref(), &mut children);
1506 let step = TraversalStep {
1507 target_labels: rel.target_labels.clone(),
1508 rel_type: rel.rel_type.clone(),
1509 direction: rel.direction,
1510 filter,
1511 };
1512 path.push(step);
1513 if let Some(alias) = &rel.alias {
1514 out.push(RelatedSpec {
1515 alias: Some(alias.trim_start_matches('$').to_string()),
1516 rel_type: rel.rel_type.clone(),
1517 direction: rel.direction,
1518 target_labels: rel.target_labels.clone(),
1519 steps: path.clone(),
1520 });
1521 }
1522 for child in &children {
1523 walk(child, path, out);
1524 }
1525 path.pop();
1526 }
1527 Expr::Field(_) => {}
1528 }
1529 }
1530 let mut specs = Vec::new();
1531 if let Some(e) = expr {
1532 let mut path: Vec<TraversalStep> = Vec::new();
1533 walk(e, &mut path, &mut specs);
1534 }
1535 specs
1536}
1537
1538fn alias_field_values(
1542 lmpg: &Lmpg,
1543 snap: &QuerySnapshot,
1544 tenant: rushdb_types::ids::TenantId,
1545 ord: u64,
1546 specs: &[RelatedSpec],
1547 field: &str,
1548 budget: Option<usize>,
1549) -> Result<Option<Vec<Json>>, QueryError> {
1550 let Some((alias, prop)) = field.split_once('.') else {
1551 return Ok(None);
1552 };
1553 let Some(related) = count_related(lmpg, snap, tenant, ord, specs, alias, budget)? else {
1554 return Ok(None);
1555 };
1556 let mut retained = related.capacity().saturating_mul(8);
1557 aggregate_budget::check(budget, retained)?;
1558 let mut values = Vec::new();
1559 let selected: std::collections::HashSet<String> =
1561 [prop.to_owned(), normalize(prop)].into_iter().collect();
1562 for other in related {
1563 let reader = aggregate_budget::RecordReader::new(
1564 snap,
1565 budget.map(|limit| limit.saturating_sub(retained)),
1566 );
1567 let mut session = reconstruct::ReconstructSession::new(&reader, tenant);
1568 if let Some(Json::Object(child)) = session.reconstruct_record_selected(other, &selected)? {
1569 if let Some(v) = resolve_field(&child, prop) {
1570 retained = retained.saturating_add(aggregate_budget::value_bytes(&v));
1571 aggregate_budget::check(budget, retained)?;
1572 values.push(v);
1573 }
1574 }
1575 }
1576 Ok(Some(values))
1577}
1578
1579pub const MISSING_SENTINEL: &str = "__missing__";
1582
1583#[derive(Debug, Clone, Default, serde::Serialize)]
1586pub struct GroupBucket {
1587 pub keys: Map<String, Json>,
1590 pub count_roots: u64,
1592 pub sums: Map<String, Json>,
1594 pub counts: Map<String, Json>,
1596 pub mins: Map<String, Json>,
1598 pub maxs: Map<String, Json>,
1600 pub derived_outputs: std::collections::BTreeMap<String, Json>,
1602}
1603
1604#[derive(Debug, Clone)]
1607pub enum OutputExpr {
1608 Ref(String),
1610 Field(String),
1612 Add(Box<OutputExpr>, Box<OutputExpr>),
1614 Subtract(Box<OutputExpr>, Box<OutputExpr>),
1616 Multiply(Box<OutputExpr>, Box<OutputExpr>),
1618 Divide {
1620 dividend: Box<OutputExpr>,
1622 divisor: Box<OutputExpr>,
1624 precision: u32,
1626 },
1627 Literal(f64),
1629}
1630
1631#[derive(Debug, Clone, Default)]
1633pub struct AggregateRequest {
1634 pub labels: Vec<String>,
1636 pub where_: Option<Expr>,
1638 pub related: Vec<RelatedSpec>,
1640 pub group_by: Vec<String>,
1642 pub sums: Vec<(String, String)>,
1644 pub counts: Vec<(String, String)>,
1647 pub mins: Vec<(String, String)>,
1649 pub maxs: Vec<(String, String)>,
1651 pub time_bucket: Option<TimeBucketSpec>,
1653 pub outputs: Vec<(String, OutputExpr)>,
1655 pub expr_sums: Vec<(String, OutputExpr)>,
1657 pub expr_avgs: Vec<(String, OutputExpr)>,
1659 pub avg_terms: Vec<(String, String)>,
1661 pub avg_precision: std::collections::BTreeMap<String, u32>,
1663 pub time_bucket_alias: Option<String>,
1665 pub plain_fields: Vec<(String, String)>,
1667 pub collects: Vec<CollectSpec>,
1670 pub order_by: Vec<(String, bool )>,
1672 pub skip: u64,
1674 pub limit: Option<u64>,
1676 pub hidden_outputs: std::collections::BTreeSet<String>,
1678}
1679
1680pub struct EvalCtx<'a> {
1683 pub lmpg: &'a Lmpg,
1685 pub snap: QuerySnapshot,
1687 pub is_cancelled: Option<&'a dyn Fn() -> bool>,
1689 pub(crate) cycles: cycle::CycleGraphs,
1691 pub(crate) declared_types:
1695 std::cell::RefCell<std::collections::HashMap<String, Option<LogicalType>>>,
1696 pub(crate) record_types:
1700 std::cell::RefCell<std::collections::HashMap<String, Vec<(u64, LogicalType)>>>,
1701}
1702
1703impl<'a> EvalCtx<'a> {
1704 pub fn new(lmpg: &'a Lmpg) -> Result<Self, QueryError> {
1711 Ok(EvalCtx {
1712 lmpg,
1713 snap: Arc::new(lmpg.store.read_view()?),
1714 is_cancelled: None,
1715 cycles: cycle::CycleGraphs::default(),
1716 declared_types: std::cell::RefCell::default(),
1717 record_types: std::cell::RefCell::default(),
1718 })
1719 }
1720
1721 pub fn check_cancelled(&self) -> Result<(), QueryError> {
1727 if self.is_cancelled.is_some_and(|probe| probe()) {
1728 Err(QueryError::Cancelled)
1729 } else {
1730 Ok(())
1731 }
1732 }
1733}
1734
1735fn eval_expr_ctx(
1736 ctx: &EvalCtx<'_>,
1737 expr: &Expr,
1738 obj: &Map<String, Json>,
1739) -> Result<bool, QueryError> {
1740 match expr {
1741 Expr::And(items) => {
1742 for item in items {
1743 if !eval_expr_ctx(ctx, item, obj)? {
1744 return Ok(false);
1745 }
1746 }
1747 Ok(true)
1748 }
1749 Expr::Or(items) | Expr::Nor(items) => {
1750 let inverted = matches!(expr, Expr::Nor(_));
1751 for item in items {
1752 if eval_expr_ctx(ctx, item, obj)? {
1753 return Ok(!inverted);
1754 }
1755 }
1756 Ok(inverted)
1757 }
1758 Expr::Xor(a, b) => Ok(eval_expr_ctx(ctx, a, obj)? != eval_expr_ctx(ctx, b, obj)?),
1759 Expr::Not(inner) => Ok(!eval_expr_ctx(ctx, inner, obj)?),
1760 Expr::Field(pred) => eval_pred(&|name| type_held(ctx, name, obj), pred, obj),
1761 Expr::Related(rel) => eval_related(ctx, obj, rel),
1762 }
1763}
1764
1765fn eval_related(
1774 ctx: &EvalCtx<'_>,
1775 root: &Map<String, Json>,
1776 rel: &RelatedExpr,
1777) -> Result<bool, QueryError> {
1778 let root_id = root.get(ID_FIELD).and_then(|v| v.as_str()).unwrap_or("");
1779 let start_ord = root_ord_of(ctx, root_id)?;
1780 if start_ord == u64::MAX {
1781 return Ok(false);
1782 }
1783 let (min_hops, max_hops) = rel.hops.unwrap_or((1, 1));
1784
1785 let neighbors = |node: u64| -> Result<Vec<(String, u64, u64)>, QueryError> {
1786 Ok(match rel.direction {
1787 Direction::Outgoing => {
1788 access::adj_out(&ctx.snap, ctx.lmpg.tenant, node, rel.rel_type.as_deref())?
1789 }
1790 Direction::Incoming => {
1791 access::adj_in(&ctx.snap, ctx.lmpg.tenant, node, rel.rel_type.as_deref())?
1792 }
1793 Direction::Both => {
1794 let mut all =
1795 access::adj_out(&ctx.snap, ctx.lmpg.tenant, node, rel.rel_type.as_deref())?;
1796 all.extend(access::adj_in(
1797 &ctx.snap,
1798 ctx.lmpg.tenant,
1799 node,
1800 rel.rel_type.as_deref(),
1801 )?);
1802 all
1803 }
1804 })
1805 };
1806
1807 let endpoint_ok = |other: u64| -> Result<bool, QueryError> {
1809 if rel.target_labels.is_empty() {
1810 Ok(true)
1811 } else {
1812 Ok(access::record_by_ord(&ctx.snap, ctx.lmpg.tenant, other)?
1813 .is_some_and(|h| rel.target_labels.contains(&h.label)))
1814 }
1815 };
1816
1817 if rel.cycle {
1818 let cancel = || ctx.check_cancelled();
1819 return ctx
1820 .cycles
1821 .on_cycle(&ctx.snap, ctx.lmpg.tenant, rel, start_ord, &cancel);
1822 }
1823
1824 let mut frontier: Vec<u64> = vec![start_ord];
1826 let mut visited: std::collections::BTreeSet<u64> = std::collections::BTreeSet::new();
1827 visited.insert(start_ord);
1828 let mut depth = 0u64;
1829
1830 while !frontier.is_empty() && depth < max_hops {
1831 ctx.check_cancelled()?;
1832 depth += 1;
1833 let mut next: Vec<u64> = Vec::new();
1834 for node in &frontier {
1835 for (_ty, other, _ro) in neighbors(*node)? {
1836 if !endpoint_ok(other)? {
1837 continue;
1838 }
1839 if depth >= min_hops {
1840 if let Some(filter) = &rel.filter {
1841 if access::record_by_ord(&ctx.snap, ctx.lmpg.tenant, other)?.is_some() {
1842 let target_obj =
1843 filtered_row(&ctx.snap, &ctx.snap, ctx.lmpg.tenant, filter, other)?;
1844 if eval_expr_ctx(ctx, filter, &target_obj)? {
1845 return Ok(true);
1846 }
1847 }
1848 } else {
1849 return Ok(true);
1850 }
1851 }
1852 if depth < max_hops && visited.insert(other) {
1853 next.push(other);
1854 }
1855 }
1856 }
1857 frontier = next;
1858 }
1859 Ok(false)
1860}
1861
1862fn root_ord_of(ctx: &EvalCtx<'_>, id_hex: &str) -> Result<u64, QueryError> {
1863 let bytes = hex_decode_16(id_hex).unwrap_or([0u8; 16]);
1864 let id = rushdb_types::ids::RecordId(bytes);
1865 Ok(access::record_by_id(&ctx.snap, ctx.lmpg.tenant, &id)?.map_or(u64::MAX, |h| h.ordinal))
1866}
1867
1868pub(crate) type DeclaredType<'a> =
1870 dyn Fn(&str) -> Result<Option<rushdb_types::model::LogicalType>, QueryError> + 'a;
1871
1872#[expect(
1873 clippy::too_many_lines,
1874 reason = "one exhaustive match over every field predicate keeps the operator semantics readable in a single place"
1875)]
1876pub(crate) fn eval_pred(
1877 declared_type: &DeclaredType<'_>,
1878 pred: &FieldPred,
1879 obj: &Map<String, Json>,
1880) -> Result<bool, QueryError> {
1881 Ok(match pred {
1882 FieldPred::Exists(name, want) => {
1883 let present = raw_field(obj, name).is_some();
1884 present == *want
1885 }
1886 FieldPred::IsNull(name, want) => {
1887 let v = raw_field(obj, name);
1888 let is_null = matches!(v, Some(Json::Null));
1889 is_null == *want
1890 }
1891 FieldPred::Eq(name, val) => match (raw_field(obj, name), val) {
1892 (Some(_), Json::Null) => matches!(raw_field(obj, name), Some(Json::Null)),
1893 (Some(actual), expected) => {
1894 let datetime = is_datetime(declared_type, name)?;
1895 any_element(actual, |el| value_equals(el, expected, datetime))
1896 }
1897 (None, _) => false,
1898 },
1899 FieldPred::Ne(name, val) => {
1900 match raw_field(obj, name) {
1902 None => false,
1903 Some(actual) => {
1904 let datetime = is_datetime(declared_type, name)?;
1905 !any_element(actual, |el| value_equals(el, val, datetime))
1906 }
1907 }
1908 }
1909 FieldPred::In(name, vals) => match raw_field(obj, name) {
1910 None => false,
1911 Some(actual) => {
1912 let datetime = is_datetime(declared_type, name)?;
1913 any_element(actual, |el| {
1914 vals.iter().any(|v| value_equals(el, v, datetime))
1915 })
1916 }
1917 },
1918 FieldPred::Contains(name, sub) => raw_field(obj, name).is_some_and(|j| {
1919 let sub = sub.to_lowercase();
1920 any_element(j, |el| {
1921 el.as_str()
1922 .is_some_and(|st| st.to_lowercase().contains(sub.as_str()))
1923 })
1924 }),
1925 FieldPred::StartsWith(name, prefix) => raw_field(obj, name).is_some_and(|j| {
1926 let prefix = prefix.to_lowercase();
1927 any_element(j, |el| {
1928 el.as_str()
1929 .is_some_and(|st| st.to_lowercase().starts_with(prefix.as_str()))
1930 })
1931 }),
1932 FieldPred::Gt(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1933 o == std::cmp::Ordering::Greater
1934 }),
1935 FieldPred::Gte(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1936 o != std::cmp::Ordering::Less
1937 }),
1938 FieldPred::Lt(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1939 o == std::cmp::Ordering::Less
1940 }),
1941 FieldPred::Lte(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1942 o != std::cmp::Ordering::Greater
1943 }),
1944 FieldPred::Nin(name, vals) => {
1945 match raw_field(obj, name) {
1947 None => false,
1948 Some(actual) => {
1949 let datetime = is_datetime(declared_type, name)?;
1950 !any_element(actual, |el| {
1951 vals.iter().any(|v| value_equals(el, v, datetime))
1952 })
1953 }
1954 }
1955 }
1956 FieldPred::EndsWith(name, suffix) => raw_field(obj, name).is_some_and(|j| {
1957 let suffix = suffix.to_lowercase();
1958 any_element(j, |el| {
1959 el.as_str()
1960 .is_some_and(|st| st.to_lowercase().ends_with(suffix.as_str()))
1961 })
1962 }),
1963 FieldPred::Id(id) => obj
1964 .get(ID_FIELD)
1965 .and_then(|v| v.as_str())
1966 .is_some_and(|s| canonical_id(s) == canonical_id(id)),
1967 FieldPred::DatetimeComponent {
1968 field,
1969 which,
1970 value,
1971 } => raw_field(obj, field)
1972 .and_then(serde_json::Value::as_i64)
1973 .is_some_and(|ms| {
1974 let actual = crate::datetime::component_of(ms, *which);
1975 actual == *value
1976 }),
1977 FieldPred::Type(name, expected) => {
1978 let want = canonical_type_alias(&expected.to_lowercase());
1979 let (base_want, want_array) = match want.strip_suffix("[]") {
1980 Some(base) if !base.is_empty() => (Some(canonical_type_alias(base)), true),
1981 _ => (None, want == "array"),
1982 };
1983 let value = raw_field(obj, name);
1984 if value.is_none() {
1987 return Ok(want == "null");
1988 }
1989 let is_array = matches!(value, Some(Json::Array(_)));
1990
1991 if want == "array" {
1993 return Ok(is_array);
1994 }
1995
1996 if let Some(ty) = declared_type(name)? {
2000 let name_of = |t: &rushdb_types::model::LogicalType| -> &'static str {
2001 match t {
2002 rushdb_types::model::LogicalType::Boolean => "boolean",
2003 rushdb_types::model::LogicalType::Int64
2004 | rushdb_types::model::LogicalType::Float64 => "number",
2005 rushdb_types::model::LogicalType::Utf8 => "string",
2006 rushdb_types::model::LogicalType::DateTime => "datetime",
2007 }
2008 };
2009 if want_array {
2010 if is_array && name_of(&ty) == base_want.as_deref().unwrap_or("") {
2013 return Ok(true);
2014 }
2015 } else if name_of(&ty) == want {
2016 return Ok(true);
2019 }
2020 }
2021
2022 let shape_of = |v: &Json| -> &'static str {
2023 if v.is_string() {
2024 "string"
2025 } else if v.is_number() {
2026 "number"
2027 } else if v.is_boolean() {
2028 "boolean"
2029 } else if v.is_array() {
2030 "array"
2031 } else if v.is_object() {
2032 "object"
2033 } else {
2034 "null"
2035 }
2036 };
2037
2038 if want_array {
2039 let base = base_want.as_deref().unwrap_or("");
2042 return Ok(
2043 matches!(value, Some(Json::Array(items)) if items.iter().any(|el| shape_of(el) == base)),
2044 );
2045 }
2046
2047 let actual = value.map_or("null", shape_of);
2048 actual == want
2049 }
2050 })
2051}
2052
2053fn numeric_cmp(
2054 value: Option<&Json>,
2055 bound: f64,
2056 accept: impl Fn(std::cmp::Ordering) -> bool,
2057) -> bool {
2058 value
2059 .and_then(infer_scalar)
2060 .and_then(|(_, scalar)| match scalar {
2061 Scalar::Int64(_) | Scalar::Float64(_) => Some(scalar),
2062 _ => None,
2063 })
2064 .and_then(|value| canonical_scalar_cmp(&value, &Scalar::Float64(bound)))
2065 .is_some_and(accept)
2066}
2067
2068fn numeric_cmp_any(
2069 value: Option<&Json>,
2070 bound: f64,
2071 accept: impl Fn(std::cmp::Ordering) -> bool,
2072) -> bool {
2073 match value {
2074 Some(v) if v.is_array() => v
2075 .as_array()
2076 .is_some_and(|a| a.iter().any(|el| numeric_cmp(Some(el), bound, &accept))),
2077 other => numeric_cmp(other, bound, accept),
2078 }
2079}
2080
2081fn canonical_type_alias(t: &str) -> String {
2085 if t == "bool" {
2086 "boolean".to_string()
2087 } else {
2088 t.to_string()
2089 }
2090}
2091
2092fn json_value_cmp(a: &Json, b: &Json) -> std::cmp::Ordering {
2097 let numeric = |v: &Json| {
2098 infer_scalar(v).and_then(|(_, scalar)| match scalar {
2099 Scalar::Int64(_) | Scalar::Float64(_) => Some(scalar),
2100 _ => None,
2101 })
2102 };
2103 match (numeric(a), numeric(b)) {
2104 (Some(x), Some(y)) => canonical_scalar_cmp(&x, &y).unwrap_or(std::cmp::Ordering::Equal),
2105 (None, Some(_)) => std::cmp::Ordering::Less,
2107 (Some(_), None) => std::cmp::Ordering::Greater,
2108 (None, None) => match (a.as_str(), b.as_str()) {
2109 (Some(x), Some(y)) => x.cmp(y),
2110 _ => format!("{a}").cmp(&format!("{b}")),
2111 },
2112 }
2113}
2114
2115fn raw_field<'a>(obj: &'a Map<String, Json>, name: &str) -> Option<&'a Json> {
2116 let norm = normalize(name);
2117 obj.iter()
2118 .find(|(k, _)| normalize(k) == norm)
2119 .map(|(_, v)| v)
2120}
2121
2122fn canonical_id(id: &str) -> String {
2123 id.bytes()
2124 .filter(|byte| *byte != b'-')
2125 .map(|byte| (byte as char).to_ascii_lowercase())
2126 .collect()
2127}
2128
2129fn binding_roots_with_cancel(
2131 lmpg: &Lmpg,
2132 req: &SearchRequest,
2133 is_cancelled: &dyn Fn() -> bool,
2134 snap: &QuerySnapshot,
2135 budget: Option<usize>,
2136) -> Result<Vec<u64>, QueryError> {
2137 let plan = index_candidates_with_budget(lmpg, snap, req, budget, is_cancelled)?;
2142 let index_exact = plan.as_ref().is_some_and(|plan| plan.exact);
2143 let candidates: Vec<u64> = match plan {
2144 Some(plan) => plan.ords.into_iter().collect(),
2145 None => scan::Candidates::walk(snap, lmpg.tenant, &req.labels)?.collect(is_cancelled)?,
2146 };
2147 let candidate_bytes = candidates.capacity().saturating_mul(16);
2149 aggregate_budget::check(budget, candidate_bytes)?;
2150 if index_exact && req.order_by.is_empty() {
2151 return Ok(candidates);
2152 }
2153 let ctx = EvalCtx {
2157 lmpg,
2158 snap: snap.clone(),
2159 is_cancelled: Some(is_cancelled),
2160 cycles: crate::cycle::CycleGraphs::default(),
2161 declared_types: std::cell::RefCell::default(),
2162 record_types: std::cell::RefCell::default(),
2163 };
2164 let predicate_fields = match &req.where_ {
2165 Some(expr) => PointFields::for_predicate(snap, lmpg.tenant, expr)?,
2166 None => None,
2167 };
2168 let mut session = reconstruct::ReconstructSession::new(snap, lmpg.tenant);
2169 let mut working = WorkingSet {
2170 limit: budget
2171 .map(|limit| u64::try_from(limit.saturating_sub(candidate_bytes)).unwrap_or(u64::MAX)),
2172 charged: 0,
2173 };
2174 let mut matched: Vec<u64> = Vec::new();
2175 for ord in candidates {
2176 if is_cancelled() {
2177 return Err(QueryError::Cancelled);
2178 }
2179 if is_match(
2180 &ctx,
2181 req,
2182 req.where_.as_ref(),
2183 predicate_fields.as_ref(),
2184 &mut session,
2185 &mut working,
2186 ord,
2187 )? {
2188 matched.push(ord);
2189 }
2190 }
2191 Ok(matched)
2192}
2193
2194#[must_use]
2196pub fn normalize(name: &str) -> String {
2197 rushdb_types::value::normalize_name(name)
2198}
2199
2200fn resolve_field(obj: &Map<String, Json>, field: &str) -> Option<Json> {
2201 let field = field.strip_prefix("$record.").unwrap_or(field);
2202 if let Some(value) = obj.get(field) {
2206 return Some(value.clone());
2207 }
2208 let norm = normalize(field);
2209 obj.iter()
2210 .find(|(k, _)| normalize(k) == norm)
2211 .map(|(_, v)| v.clone())
2212}
2213
2214fn resolve_field_numeric(obj: &Map<String, Json>, field: &str) -> Option<Json> {
2215 resolve_field(obj, field).filter(serde_json::Value::is_number)
2216}
2217
2218#[cfg(test)]
2219mod index_planner_tests;