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 adjacency;
23mod aggregate_budget;
24mod aggregate_rows;
25mod aggregate_scan;
26mod budget;
27pub use budget::Budget;
28use budget::{Cap, WorkingSet};
29mod aggregate_catalog;
30mod aggregation;
31mod cycle;
32mod equality;
33mod expand;
34mod field_types;
35#[cfg(test)]
36use aggregation::exact_f64;
37pub use aggregation::MAX_AGGREGATE_BUCKETS;
38pub use aggregation::{
39 aggregate, aggregate_in_view, aggregate_to_rows, aggregate_to_rows_in_view,
40 aggregate_to_rows_in_view_with_budget, aggregate_with_cancel, aggregate_with_cancel_in_view,
41};
42use field_types::type_held;
43mod page;
44mod planned;
45mod planner;
46mod point_fields;
47mod projection;
48pub use point_fields::PointFields;
49mod property_predicate;
50pub use hex::hex_decode_16;
51pub use index_access::explain_index;
52pub use planned::{planned_candidates, PlannedCandidates};
53use planner::index_candidates_with_budget;
54pub use planner::{
55 explain_cardinality, explain_expansion, explain_range_selectivity, explain_walk,
56 unindexed_walks_total, UNINDEXED_WALK_THRESHOLD,
57};
58pub use property_predicate::{matches_properties, parse_property_predicate};
59pub mod datetime;
60pub mod document;
61mod hex;
62mod index_access;
63mod scan;
64mod stats_cache;
65pub use document::{ParsedQuery, QueryDocumentError};
66
67#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
69pub enum FieldPred {
70 Eq(String, Json),
72 Ne(String, Json),
74 Gt(String, f64),
76 Gte(String, f64),
78 Lt(String, f64),
80 Lte(String, f64),
82 Contains(String, String),
84 StartsWith(String, String),
86 Exists(String, bool),
88 IsNull(String, bool),
90 In(String, Vec<Json>),
92 Nin(String, Vec<Json>),
94 EndsWith(String, String),
96 Id(String),
98 DatetimeComponent {
101 field: String,
103 which: u8,
105 value: i64,
107 },
108 Type(String, String),
110}
111
112#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
114pub enum Expr {
115 And(Vec<Expr>),
117 Or(Vec<Expr>),
119 Xor(Box<Expr>, Box<Expr>),
121 Nor(Vec<Expr>),
123 Not(Box<Expr>),
125 Field(FieldPred),
127 Related(RelatedExpr),
130}
131
132#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
134pub struct RelatedExpr {
135 pub target_labels: Vec<String>,
137 pub alias: Option<String>,
139 pub rel_type: Option<String>,
141 pub direction: Direction,
143 pub hops: Option<(u64, u64)>,
145 pub cycle: bool,
147 pub filter: Option<Box<Expr>>,
150}
151
152#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
154pub struct RelatedSpec {
155 pub alias: Option<String>,
157 pub rel_type: Option<String>,
159 pub direction: Direction,
161 pub target_labels: Vec<String>,
163 pub steps: Vec<TraversalStep>,
169}
170
171#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
173pub struct TraversalStep {
174 pub target_labels: Vec<String>,
176 pub rel_type: Option<String>,
178 pub direction: Direction,
180 pub filter: Option<Expr>,
183}
184
185#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
196pub struct CollectSpec {
197 pub alias: String,
199 pub rel_type: Option<String>,
201 pub direction: Direction,
203 pub target_labels: Vec<String>,
205 pub steps: Vec<TraversalStep>,
209 pub fields: Vec<(String, String)>,
211 pub nested: Vec<CollectSpec>,
213 pub filter: Option<Expr>,
215 pub order_by: Vec<(String, bool)>,
217 pub unique: bool,
219 pub skip: u64,
221 pub limit: Option<u64>,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
227pub enum Direction {
228 Outgoing,
230 Incoming,
232 Both,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
238pub enum TimeUnit {
239 Seconds,
241 Minutes,
243 Hours,
245 Days,
247 Weeks,
249 Months,
251 Quarters,
253 Years,
255}
256
257impl TimeUnit {
258 fn millis(self) -> i64 {
259 match self {
260 TimeUnit::Seconds => 1_000,
261 TimeUnit::Minutes => 60_000,
262 TimeUnit::Hours => 3_600_000,
263 TimeUnit::Days => 86_400_000,
264 TimeUnit::Weeks => 7 * 86_400_000,
265 TimeUnit::Months => 30 * 86_400_000,
266 TimeUnit::Quarters => 90 * 86_400_000,
267 TimeUnit::Years => 365 * 86_400_000,
268 }
269 }
270
271 pub fn from_name(name: &str) -> Result<TimeUnit, crate::QueryDocumentError> {
278 Ok(match name {
279 "second" | "seconds" => TimeUnit::Seconds,
280 "minute" | "minutes" => TimeUnit::Minutes,
281 "hour" | "hours" => TimeUnit::Hours,
282 "day" | "days" => TimeUnit::Days,
283 "week" | "weeks" => TimeUnit::Weeks,
284 "month" | "months" => TimeUnit::Months,
285 "quarter" => TimeUnit::Quarters,
286 "year" | "years" => TimeUnit::Years,
287 other => {
288 return Err(crate::QueryDocumentError::Parse(format!(
289 "unknown time unit `{other}`"
290 )));
291 }
292 })
293 }
294
295 pub(crate) fn unit_str(self) -> &'static str {
296 match self {
297 TimeUnit::Seconds => "seconds",
298 TimeUnit::Minutes => "minutes",
299 TimeUnit::Hours => "hours",
300 TimeUnit::Days => "days",
301 TimeUnit::Weeks => "weeks",
302 TimeUnit::Months => "months",
303 TimeUnit::Quarters => "quarters",
304 TimeUnit::Years => "years",
305 }
306 }
307}
308
309#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
312pub struct TimeBucketSpec {
313 pub field: String,
315 pub unit: TimeUnit,
317 pub size: u64,
319}
320
321pub const MAX_AVG_PRECISION: u32 = 15;
325
326impl TimeBucketSpec {
327 pub fn fixed(field: impl Into<String>, unit: TimeUnit) -> Self {
329 TimeBucketSpec {
330 field: field.into(),
331 unit,
332 size: 1,
333 }
334 }
335
336 pub fn sized(field: impl Into<String>, unit: TimeUnit, size: u64) -> Self {
342 assert!(size > 0, "timeBucket size must be positive");
343 TimeBucketSpec {
344 field: field.into(),
345 unit,
346 size,
347 }
348 }
349
350 #[expect(
351 clippy::cast_possible_truncation,
352 reason = "epoch-ms is a bounded timestamp; the saturating float-to-int cast matches the reference calendar flooring"
353 )]
354 fn floor_millis(&self, epoch_ms: f64) -> i64 {
355 let ms = epoch_ms as i64;
356 match self.unit {
360 TimeUnit::Months => rushdb_types::datetime::start_of_month(ms),
361 TimeUnit::Quarters => rushdb_types::datetime::start_of_quarter(ms),
362 TimeUnit::Years => rushdb_types::datetime::start_of_year(ms),
363 _ => {
364 let size = i64::try_from(self.size).unwrap_or(i64::MAX).max(1);
367 let width = self.unit.millis().saturating_mul(size).max(1);
368 ms.div_euclid(width).saturating_mul(width)
369 }
370 }
371 }
372}
373
374#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize)]
376pub struct Pagination {
377 pub skip: u64,
379 pub limit: Option<u64>,
381}
382
383#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
385pub struct SearchRequest {
386 #[serde(default)]
388 pub labels: Vec<String>,
389 #[serde(default, rename = "where")]
391 pub where_: Option<Expr>,
392 #[serde(default)]
395 pub related: Vec<RelatedSpec>,
396 #[serde(default)]
398 pub collects: Vec<CollectSpec>,
399 #[serde(default, skip_serializing_if = "Vec::is_empty")]
402 pub projection: Vec<(String, String)>,
403 #[serde(default)]
405 pub pagination: Pagination,
406 #[serde(default)]
408 pub order_by: Vec<(String, bool)>,
409 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
413 pub force_scan: bool,
414}
415
416#[derive(Debug, Error)]
418pub enum QueryError {
419 #[error("storage error: {0}")]
421 Storage(#[from] rushdb_storage::StorageError),
422 #[error("engine error: {0}")]
424 Engine(#[from] LmpgError),
425 #[error("invalid predicate on `{0}`")]
427 InvalidPredicate(String),
428 #[error("query cancelled")]
430 Cancelled,
431 #[error("resource exhausted: {0}")]
433 ResourceLimit(String),
434 #[error("unindexed walk refused: {0}")]
437 UnindexedWalk(String),
438}
439
440#[derive(Debug, Clone)]
442pub struct Page {
443 pub total_matched: u64,
445 pub records: Vec<Json>,
447}
448
449pub fn find_records(lmpg: &Lmpg, req: &SearchRequest) -> Result<Page, QueryError> {
457 find_records_with_cancel(lmpg, req, &|| false)
458}
459
460pub fn find_records_with_cancel(
469 lmpg: &Lmpg,
470 req: &SearchRequest,
471 is_cancelled: &dyn Fn() -> bool,
472) -> Result<Page, QueryError> {
473 let snap = Arc::new(lmpg.store.read_view()?);
474 find_records_in_view(lmpg, req, is_cancelled, snap)
475}
476
477#[expect(
491 clippy::needless_pass_by_value,
492 reason = "callers hand over a freshly pinned view, matching find_records_in_view"
493)]
494pub fn matching_ordinals(
495 lmpg: &Lmpg,
496 req: &SearchRequest,
497 is_cancelled: &dyn Fn() -> bool,
498 snap: QuerySnapshot,
499) -> Result<Vec<u64>, QueryError> {
500 let is_cancelled: &dyn Fn() -> bool = &cancel_probe(lmpg, is_cancelled);
501 if !snap.belongs_to(&lmpg.store) {
502 return Err(rushdb_storage::StorageError::InvalidRead(
503 "query view belongs to another Store".into(),
504 )
505 .into());
506 }
507 if is_cancelled() {
508 return Err(QueryError::Cancelled);
509 }
510 let mut full = req.clone();
511 full.pagination = Pagination::default();
512 Ok(run_matches(lmpg, &full, is_cancelled, &snap, usize::MAX, None)?.retained)
513}
514
515#[expect(
524 clippy::needless_pass_by_value,
525 reason = "callers hand over a freshly pinned view, matching find_records_in_view"
526)]
527pub fn matching_ordinals_first(
528 lmpg: &Lmpg,
529 req: &SearchRequest,
530 is_cancelled: &dyn Fn() -> bool,
531 snap: QuerySnapshot,
532 keep: usize,
533) -> Result<Vec<u64>, QueryError> {
534 let is_cancelled: &dyn Fn() -> bool = &cancel_probe(lmpg, is_cancelled);
535 if !snap.belongs_to(&lmpg.store) {
536 return Err(rushdb_storage::StorageError::InvalidRead(
537 "query view belongs to another Store".into(),
538 )
539 .into());
540 }
541 if is_cancelled() {
542 return Err(QueryError::Cancelled);
543 }
544 let mut full = req.clone();
545 full.pagination = Pagination::default();
546 Ok(run_matches(lmpg, &full, is_cancelled, &snap, keep, None)?.retained)
547}
548
549struct Matches {
551 retained: Vec<u64>,
553 total: u64,
555}
556
557fn run_matches(
569 lmpg: &Lmpg,
570 req: &SearchRequest,
571 is_cancelled: &dyn Fn() -> bool,
572 snap: &QuerySnapshot,
573 keep: usize,
574 known_total: Option<u64>,
575) -> Result<Matches, QueryError> {
576 check_walk_budget(lmpg, snap, req)?;
577 let mut working = WorkingSet {
578 limit: working_set_budget(lmpg).map(|bytes| u64::try_from(bytes).unwrap_or(u64::MAX)),
579 charged: 0,
580 };
581 let CandidateSource {
582 mut candidates,
583 exact,
584 known,
585 predicate,
586 } = candidate_source(lmpg, req, is_cancelled, snap, known_total, &mut working)?;
587 let ctx = EvalCtx::at(lmpg, snap.clone(), Some(is_cancelled));
588 let mut session = reconstruct::ReconstructSession::new(snap, lmpg.tenant);
589 let order_fields = if req.order_by.is_empty() {
590 None
591 } else {
592 PointFields::resolve_in(
593 snap,
594 lmpg.tenant,
595 &req.labels,
596 req.order_by.iter().map(|(f, _)| f.as_str()),
597 )?
598 };
599 let predicate_fields = match &predicate {
600 Some(expr) => PointFields::for_predicate(snap, lmpg.tenant, &req.labels, expr)?,
601 None => None,
602 };
603 let mut retained: Vec<u64> = Vec::new();
604 let mut ordered: Vec<OrderRow> = Vec::new();
607 let mut ordered_bytes: Vec<u64> = Vec::new();
608 let mut total: u64 = 0;
609 while let Some(ord) = candidates.next(is_cancelled)? {
610 if is_cancelled() {
611 return Err(QueryError::Cancelled);
612 }
613 if !exact
614 && !is_match(
615 &ctx,
616 req,
617 predicate.as_ref(),
618 predicate_fields.as_ref(),
619 &mut session,
620 &mut working,
621 ord,
622 )?
623 {
624 continue;
625 }
626 total += 1;
627 if req.order_by.is_empty() {
628 if retained.len() < keep {
629 working.charge(8, true)?;
630 retained.push(ord);
631 } else if known.is_some() {
632 break;
633 }
634 continue;
635 }
636 let row = read_row(order_fields.as_ref(), &mut session, snap, lmpg.tenant, ord)?;
637 let bytes =
638 serde_json::to_vec(&row).map_or(0, |v| u64::try_from(v.len()).unwrap_or(u64::MAX));
639 working.charge(bytes, true)?;
640 ordered.push((ord, row));
641 ordered_bytes.push(bytes);
642 if ordered.len() >= keep.saturating_mul(2).max(64) {
643 working.charged = working.charged.saturating_sub(cut_ordered(
644 &mut ordered,
645 &mut ordered_bytes,
646 req,
647 keep,
648 ));
649 }
650 }
651 if !req.order_by.is_empty() {
652 cut_ordered(&mut ordered, &mut ordered_bytes, req, keep);
653 retained = ordered.into_iter().map(|(ord, _)| ord).collect();
654 }
655 Ok(Matches {
656 retained,
657 total: known.unwrap_or(total),
658 })
659}
660
661fn is_match(
667 ctx: &EvalCtx<'_>,
668 req: &SearchRequest,
669 predicate: Option<&Expr>,
670 fields: Option<&PointFields>,
671 session: &mut reconstruct::ReconstructSession<'_, QuerySnapshot>,
672 working: &mut WorkingSet,
673 ord: u64,
674) -> Result<bool, QueryError> {
675 let (snap, tenant) = (&ctx.snap, ctx.lmpg.tenant);
676 if let Some(expr) = predicate {
677 let obj = read_row(fields, session, snap, tenant, ord)?;
678 let bytes =
679 serde_json::to_vec(&obj).map_or(0, |v| u64::try_from(v.len()).unwrap_or(u64::MAX));
680 working.charge(bytes, false)?;
681 if !eval_expr_ctx(ctx, expr, &obj, ord)? {
682 return Ok(false);
683 }
684 }
685 for spec in &req.related {
686 if !related_exists(snap, tenant, ord, spec)? {
687 return Ok(false);
688 }
689 }
690 Ok(true)
691}
692
693fn filtered_row<R: rushdb_lmpg::reader::TryRead>(
698 reader: &R,
699 snap: &QuerySnapshot,
700 tenant: rushdb_types::ids::TenantId,
701 predicate: &Expr,
702 ord: u64,
703) -> Result<Map<String, Json>, QueryError> {
704 let mut session = reconstruct::ReconstructSession::new(reader, tenant);
705 let mut names = std::collections::BTreeSet::new();
706 let record = if point_fields::predicate_fields(predicate, &mut names) {
707 let selected: std::collections::HashSet<String> = names
708 .iter()
709 .flat_map(|name| [name.clone(), normalize(name)])
710 .collect();
711 session.reconstruct_record_selected(ord, &selected)?
712 } else {
713 session.reconstruct_record(ord)?
714 };
715 let _ = snap;
716 Ok(match record {
717 Some(Json::Object(map)) => map,
718 _ => Map::new(),
719 })
720}
721
722fn read_row(
724 fields: Option<&PointFields>,
725 session: &mut reconstruct::ReconstructSession<'_, QuerySnapshot>,
726 snap: &QuerySnapshot,
727 tenant: rushdb_types::ids::TenantId,
728 ord: u64,
729) -> Result<Map<String, Json>, QueryError> {
730 match fields {
731 Some(fields) => fields.row(snap, tenant, ord),
732 None => Ok(session
733 .reconstruct_record(ord)?
734 .and_then(|record| match record {
735 Json::Object(map) => Some(map),
736 _ => None,
737 })
738 .unwrap_or_default()),
739 }
740}
741
742struct CandidateSource<'v> {
744 candidates: scan::Candidates<'v>,
745 exact: bool,
748 known: Option<u64>,
750 predicate: Option<Expr>,
753}
754
755fn candidate_source<'v>(
758 lmpg: &Lmpg,
759 req: &SearchRequest,
760 is_cancelled: &dyn Fn() -> bool,
761 snap: &'v QuerySnapshot,
762 known_total: Option<u64>,
763 working: &mut WorkingSet,
764) -> Result<CandidateSource<'v>, QueryError> {
765 if let Some(plan) =
766 index_candidates_with_budget(lmpg, snap, req, working_set_budget(lmpg), is_cancelled)?
767 {
768 working.charge(
769 u64::try_from(plan.ords.serialized_size()).unwrap_or(u64::MAX),
770 true,
771 )?;
772 let known = known_total
773 .or(plan.known_total)
774 .or_else(|| plan.exact.then(|| plan.ords.len()));
775 return Ok(CandidateSource {
776 candidates: scan::Candidates::from_set(plan.ords),
777 exact: plan.exact,
778 known,
779 predicate: plan.residual,
780 });
781 }
782 let exact = req.where_.is_none() && req.related.is_empty();
783 let known = match known_total {
784 Some(total) => Some(total),
785 None if exact => Some(walk_total(lmpg, snap, &req.labels)?),
786 None => None,
787 };
788 Ok(CandidateSource {
789 candidates: scan::Candidates::walk(snap, lmpg.tenant, &req.labels)?,
790 exact,
791 known,
792 predicate: req.where_.clone(),
793 })
794}
795
796type OrderRow = (u64, Map<String, Json>);
798
799fn cut_ordered(
802 ordered: &mut Vec<OrderRow>,
803 bytes: &mut Vec<u64>,
804 req: &SearchRequest,
805 keep: usize,
806) -> u64 {
807 let mut rows: Vec<(OrderRow, u64)> = ordered.drain(..).zip(bytes.drain(..)).collect();
808 sort_ordered_rows(&mut rows, req);
812 let freed = rows.iter().skip(keep).map(|(_, b)| *b).sum();
813 rows.truncate(keep);
814 for (row, b) in rows {
815 ordered.push(row);
816 bytes.push(b);
817 }
818 freed
819}
820
821fn walk_total(lmpg: &Lmpg, snap: &QuerySnapshot, labels: &[String]) -> Result<u64, QueryError> {
823 if labels.is_empty() {
824 return Ok(rushdb_lmpg::derived::tenant_usage(snap.as_ref(), lmpg.tenant)?.records);
825 }
826 let distinct: std::collections::BTreeSet<&String> = labels.iter().collect();
827 let mut total = 0u64;
828 for label in distinct {
829 total = total.saturating_add(rushdb_lmpg::derived::label_count(
830 snap.as_ref(),
831 lmpg.tenant,
832 label,
833 )?);
834 }
835 Ok(total)
836}
837
838pub fn find_records_in_view(
848 lmpg: &Lmpg,
849 req: &SearchRequest,
850 is_cancelled: &dyn Fn() -> bool,
851 snap: QuerySnapshot,
852) -> Result<Page, QueryError> {
853 find_records_in_view_with_budget(lmpg, req, is_cancelled, snap, None)
854}
855
856fn check_walk_budget(
862 lmpg: &Lmpg,
863 snap: &QuerySnapshot,
864 req: &SearchRequest,
865) -> Result<(), QueryError> {
866 let (Some(limit), Some(expr)) = (lmpg.limits().unindexed_walk_max_owners(), &req.where_) else {
867 return Ok(());
868 };
869 if explain_index(snap.as_ref(), lmpg.tenant, &req.labels, expr)?.is_some()
870 || explain_expansion(snap.as_ref(), lmpg.tenant, &req.labels, expr)?.is_some()
871 {
872 return Ok(());
873 }
874 let walk = explain_walk(snap.as_ref(), lmpg.tenant, &req.labels, expr)?;
875 let (property, owners, labels) = if let Some(walk) = walk {
876 walk
877 } else {
878 let owners = if req.labels.is_empty() {
880 rushdb_lmpg::derived::tenant_usage(snap.as_ref(), lmpg.tenant)?.records
881 } else {
882 let mut owners = 0u64;
883 for label in &req.labels {
884 owners = owners.saturating_add(rushdb_lmpg::derived::label_count(
885 snap.as_ref(),
886 lmpg.tenant,
887 label,
888 )?);
889 }
890 owners
891 };
892 (
893 "(no indexable condition)".to_owned(),
894 owners,
895 req.labels.clone(),
896 )
897 };
898 if owners <= limit {
899 return Ok(());
900 }
901 Err(QueryError::UnindexedWalk(format!(
902 "the filter on `{property}` would check {owners} records of [{}], over \
903 the unindexed-walk limit ({limit}); declare an ordered index on the property",
904 labels.join(", ")
905 )))
906}
907
908fn working_set_budget(lmpg: &Lmpg) -> Option<usize> {
915 lmpg.limits()
916 .query_memory_max_bytes()
917 .map(|bytes| usize::try_from(bytes).unwrap_or(usize::MAX))
918}
919
920pub(crate) fn cancel_probe<'a>(
923 lmpg: &Lmpg,
924 is_cancelled: &'a dyn Fn() -> bool,
925) -> impl Fn() -> bool + 'a {
926 let limits = lmpg.limits();
927 move || is_cancelled() || limits.expired()
928}
929
930#[allow(clippy::needless_pass_by_value)]
938pub fn find_records_in_view_with_budget(
939 lmpg: &Lmpg,
940 req: &SearchRequest,
941 is_cancelled: &dyn Fn() -> bool,
942 snap: QuerySnapshot,
943 work_budget: Option<usize>,
944) -> Result<Page, QueryError> {
945 find_page(lmpg, req, is_cancelled, &snap, work_budget, None)
946}
947
948pub fn find_records_streaming(
956 lmpg: &Lmpg,
957 req: &SearchRequest,
958 is_cancelled: &dyn Fn() -> bool,
959 snap: QuerySnapshot,
960) -> Result<Page, QueryError> {
961 find_records_streaming_with_total(lmpg, req, is_cancelled, snap, None)
962}
963
964#[expect(
975 clippy::needless_pass_by_value,
976 reason = "the snapshot is accepted by value as part of the public API contract so callers can hand over a freshly pinned view"
977)]
978pub fn find_records_streaming_with_total(
979 lmpg: &Lmpg,
980 req: &SearchRequest,
981 is_cancelled: &dyn Fn() -> bool,
982 snap: QuerySnapshot,
983 known_total: Option<u64>,
984) -> Result<Page, QueryError> {
985 find_page(lmpg, req, is_cancelled, &snap, None, known_total)
986}
987
988fn find_page(
991 lmpg: &Lmpg,
992 req: &SearchRequest,
993 is_cancelled: &dyn Fn() -> bool,
994 snap: &QuerySnapshot,
995 work_budget: Option<usize>,
996 known_total: Option<u64>,
997) -> Result<Page, QueryError> {
998 let is_cancelled: &dyn Fn() -> bool = &cancel_probe(lmpg, is_cancelled);
999 if !snap.belongs_to(&lmpg.store) {
1000 return Err(rushdb_storage::StorageError::InvalidRead(
1001 "query view belongs to another Store".into(),
1002 )
1003 .into());
1004 }
1005 if is_cancelled() {
1006 return Err(QueryError::Cancelled);
1007 }
1008 let skip = usize::try_from(req.pagination.skip).unwrap_or(usize::MAX);
1009 let limit = usize::try_from(req.pagination.limit.unwrap_or(u64::MAX)).unwrap_or(usize::MAX);
1010 let matches = run_matches(
1011 lmpg,
1012 req,
1013 is_cancelled,
1014 snap,
1015 skip.saturating_add(limit),
1016 known_total,
1017 )?;
1018 let page_roots: Vec<u64> = matches
1019 .retained
1020 .into_iter()
1021 .skip(skip)
1022 .take(limit)
1023 .collect();
1024 let budget = Cap::for_response(work_budget, lmpg.limits().query_memory_max_bytes());
1025 let records =
1026 page::reconstruct_page_with_budget(lmpg, req, is_cancelled, snap, page_roots, budget)?;
1027 Ok(Page {
1028 total_matched: matches.total,
1029 records,
1030 })
1031}
1032
1033fn sort_ordered_rows<T>(sortable: &mut [(OrderRow, T)], req: &SearchRequest) {
1036 for (field, desc) in req.order_by.iter().rev() {
1037 let field = normalize(field);
1038 sortable.sort_by(|((_, a), _), ((_, b), _)| {
1039 let va = a
1040 .iter()
1041 .find(|(k, _)| normalize(k) == field)
1042 .map(|(_, v)| v);
1043 let vb = b
1044 .iter()
1045 .find(|(k, _)| normalize(k) == field)
1046 .map(|(_, v)| v);
1047 let ord = match (va, vb) {
1048 (Some(x), Some(y)) => json_value_cmp(x, y),
1049 (None, Some(_)) => std::cmp::Ordering::Less,
1050 (Some(_), None) => std::cmp::Ordering::Greater,
1051 (None, None) => std::cmp::Ordering::Equal,
1052 };
1053 if *desc {
1054 ord.reverse()
1055 } else {
1056 ord
1057 }
1058 });
1059 }
1060}
1061
1062type RelatedItem = (u64, Map<String, Json>, Map<String, Json>);
1064
1065fn collect_neighbors(
1066 lmpg: &Lmpg,
1067 snap: &QuerySnapshot,
1068 root: u64,
1069 spec: &CollectSpec,
1070 budget: Option<usize>,
1071 is_cancelled: &dyn Fn() -> bool,
1072) -> Result<Vec<u64>, QueryError> {
1073 Ok(if spec.steps.is_empty() {
1074 let edges: Vec<(String, u64, u64)> = match spec.direction {
1075 Direction::Outgoing => {
1076 access::adj_out(snap, lmpg.tenant, root, spec.rel_type.as_deref())?
1077 }
1078 Direction::Incoming => {
1079 access::adj_in(snap, lmpg.tenant, root, spec.rel_type.as_deref())?
1080 }
1081 Direction::Both => {
1082 let mut all = access::adj_out(snap, lmpg.tenant, root, spec.rel_type.as_deref())?;
1083 all.extend(access::adj_in(
1084 snap,
1085 lmpg.tenant,
1086 root,
1087 spec.rel_type.as_deref(),
1088 )?);
1089 all
1090 }
1091 };
1092 edges.into_iter().map(|(_, other, _)| other).collect()
1093 } else {
1094 traverse_alias(
1095 lmpg,
1096 snap,
1097 lmpg.tenant,
1098 root,
1099 &spec.steps,
1100 budget,
1101 is_cancelled,
1102 )?
1103 })
1104}
1105
1106fn order_collected_items(items: &mut Vec<RelatedItem>, spec: &CollectSpec) {
1107 if spec.unique {
1109 items.sort_by_key(|(o, _, _)| *o);
1110 items.dedup_by(|a, b| a.1.get(ID_FIELD) == b.1.get(ID_FIELD));
1111 } else {
1112 items.sort_by_key(|(o, _, _)| *o);
1113 }
1114 if !spec.order_by.is_empty() {
1116 items.sort_by(|a, b| {
1117 for (field, desc) in &spec.order_by {
1118 let av = raw_field(&a.2, field).or_else(|| raw_field(&a.1, field));
1119 let bv = raw_field(&b.2, field).or_else(|| raw_field(&b.1, field));
1120 let ord = scalar_cmp(av, bv).unwrap_or(std::cmp::Ordering::Equal);
1121 if ord != std::cmp::Ordering::Equal {
1122 return if *desc { ord.reverse() } else { ord };
1123 }
1124 }
1125 std::cmp::Ordering::Equal
1126 });
1127 }
1128}
1129
1130fn attach_collects_with_budget(
1131 lmpg: &Lmpg,
1132 snap: &QuerySnapshot,
1133 root: u64,
1134 mut record: Json,
1135 collects: &[CollectSpec],
1136 budget: Option<Cap>,
1137 is_cancelled: &dyn Fn() -> bool,
1138) -> Result<Json, QueryError> {
1139 if collects.is_empty() {
1140 return Ok(record);
1141 }
1142 let mut retained = aggregate_budget::value_bytes(&record);
1143 Cap::check_opt(budget, retained)?;
1144 let ctx = EvalCtx::at(lmpg, snap.clone(), None);
1145 if let Some(obj) = record.as_object_mut() {
1146 for spec in collects {
1147 if is_cancelled() {
1151 return Err(QueryError::Cancelled);
1152 }
1153 let neighbors = collect_neighbors(
1154 lmpg,
1155 snap,
1156 root,
1157 spec,
1158 budget.map(|cap| cap.after(retained).remaining()),
1159 is_cancelled,
1160 )?;
1161 let base_bytes = retained;
1163 retained = retained.saturating_add(neighbors.capacity().saturating_mul(8));
1164 Cap::check_opt(budget, retained)?;
1165 let selected = point_fields::collect_selection(spec);
1168 let mut items: Vec<RelatedItem> = Vec::new();
1169 for other in neighbors {
1170 if is_cancelled() {
1171 return Err(QueryError::Cancelled);
1172 }
1173 if !has_target_label(snap, lmpg.tenant, other, &spec.target_labels)? {
1174 continue;
1175 }
1176 let Some(child) = aggregate_budget::collected_child(
1177 snap,
1178 lmpg.tenant,
1179 other,
1180 selected.as_ref(),
1181 budget.map(|cap| cap.after(retained).remaining()),
1182 )?
1183 else {
1184 continue;
1185 };
1186 if let Some(filter) = &spec.filter {
1188 if !eval_expr_ctx(&ctx, filter, &child, other)? {
1189 continue;
1190 }
1191 }
1192 retained = retained
1193 .saturating_add(aggregate_budget::object_bytes(&child).saturating_mul(3));
1194 Cap::check_opt(budget, retained)?;
1195 let projected = match project_fields_map(Json::Object(child.clone()), &spec.fields)
1196 {
1197 Json::Object(p) => p,
1198 _ => child.clone(),
1199 };
1200 items.push((other, child, projected));
1201 }
1202 order_collected_items(&mut items, spec);
1203 let start = usize::try_from(spec.skip)
1205 .unwrap_or(usize::MAX)
1206 .min(items.len());
1207 let end = spec.limit.map_or(items.len(), |l| {
1208 start
1209 .saturating_add(usize::try_from(l).unwrap_or(usize::MAX))
1210 .min(items.len())
1211 });
1212 let mut projected = Vec::new();
1213 for (other, raw, child) in items.into_iter().take(end).skip(start) {
1214 retained =
1215 retained.saturating_sub(aggregate_budget::object_bytes(&raw).saturating_mul(3));
1216 let child = attach_collects_with_budget(
1217 lmpg,
1218 snap,
1219 other,
1220 Json::Object(child),
1221 &spec.nested,
1222 budget.map(|cap| cap.after(retained)),
1223 is_cancelled,
1224 )?;
1225 retained = retained.saturating_add(aggregate_budget::value_bytes(&child));
1226 Cap::check_opt(budget, retained)?;
1227 projected.push(child);
1228 }
1229 let collected = Json::Array(projected);
1230 retained = base_bytes
1231 .saturating_add(spec.alias.len())
1232 .saturating_add(aggregate_budget::value_bytes(&collected));
1233 Cap::check_opt(budget, retained)?;
1234 obj.insert(spec.alias.clone(), collected);
1235 }
1236 }
1237 Ok(record)
1238}
1239
1240fn has_target_label(
1245 snap: &QuerySnapshot,
1246 tenant: rushdb_types::ids::TenantId,
1247 ordinal: u64,
1248 target_labels: &[String],
1249) -> Result<bool, QueryError> {
1250 if target_labels.is_empty() {
1251 return Ok(true);
1252 }
1253 Ok(access::record_by_ord(snap, tenant, ordinal)?
1254 .is_some_and(|header| target_labels.contains(&header.label)))
1255}
1256
1257fn project_fields_map(value: Json, fields: &[(String, String)]) -> Json {
1258 if fields.is_empty() {
1259 return value;
1260 }
1261 let mut out = Map::new();
1262 if let Json::Object(obj) = value {
1263 for (out_key, source) in fields {
1264 if let Some(v) = resolve_field(&obj, source) {
1265 out.insert(out_key.clone(), v);
1266 }
1267 }
1268 }
1269 Json::Object(out)
1270}
1271
1272fn scalar_cmp(a: Option<&Json>, b: Option<&Json>) -> Option<std::cmp::Ordering> {
1275 let (Some(a), Some(b)) = (a, b) else {
1276 return None;
1277 };
1278 match (a, b) {
1279 (Json::Number(_), Json::Number(_)) => {
1280 let left = infer_scalar(a).map(|(_, scalar)| scalar);
1281 let right = infer_scalar(b).map(|(_, scalar)| scalar);
1282 left.as_ref()
1283 .zip(right.as_ref())
1284 .and_then(|(left, right)| canonical_scalar_cmp(left, right))
1285 }
1286 (Json::String(x), Json::String(y)) => Some(x.cmp(y)),
1287 _ => None,
1288 }
1289}
1290
1291fn related_exists(
1292 snap: &QuerySnapshot,
1293 tenant: rushdb_types::ids::TenantId,
1294 ord: u64,
1295 spec: &RelatedSpec,
1296) -> Result<bool, QueryError> {
1297 let edges: Vec<(String, u64, u64)> = match spec.direction {
1298 Direction::Outgoing => access::adj_out(snap, tenant, ord, spec.rel_type.as_deref())?,
1299 Direction::Incoming => access::adj_in(snap, tenant, ord, spec.rel_type.as_deref())?,
1300 Direction::Both => {
1301 let mut all = access::adj_out(snap, tenant, ord, spec.rel_type.as_deref())?;
1302 all.extend(access::adj_in(snap, tenant, ord, spec.rel_type.as_deref())?);
1303 all
1304 }
1305 };
1306 if spec.target_labels.is_empty() {
1307 return Ok(!edges.is_empty());
1308 }
1309 for (_, other_ord, _) in edges {
1310 if access::record_by_ord(snap, tenant, other_ord)?
1311 .is_some_and(|h| spec.target_labels.contains(&h.label))
1312 {
1313 return Ok(true);
1314 }
1315 }
1316 Ok(false)
1317}
1318
1319fn count_related(
1325 lmpg: &Lmpg,
1326 snap: &QuerySnapshot,
1327 tenant: rushdb_types::ids::TenantId,
1328 ord: u64,
1329 specs: &[RelatedSpec],
1330 alias: &str,
1331 budget: Option<usize>,
1332) -> Result<Option<Vec<u64>>, QueryError> {
1333 let Some(spec) = specs.iter().find(|s| {
1334 s.alias
1335 .as_deref()
1336 .is_some_and(|a| a.trim_start_matches('$') == alias)
1337 }) else {
1338 return Ok(None);
1339 };
1340 Ok(Some(traverse_alias(
1341 lmpg,
1342 snap,
1343 tenant,
1344 ord,
1345 &spec.steps,
1346 budget,
1347 &|| false,
1348 )?))
1349}
1350
1351fn traverse_alias(
1355 lmpg: &Lmpg,
1356 snap: &QuerySnapshot,
1357 tenant: rushdb_types::ids::TenantId,
1358 ord: u64,
1359 steps: &[TraversalStep],
1360 budget: Option<usize>,
1361 is_cancelled: &dyn Fn() -> bool,
1362) -> Result<Vec<u64>, QueryError> {
1363 if steps.is_empty() {
1364 return Ok(Vec::new());
1365 }
1366 let ctx = EvalCtx::at(lmpg, snap.clone(), Some(is_cancelled));
1367 let mut cur = vec![ord];
1368 for step in steps {
1369 if is_cancelled() {
1370 return Err(QueryError::Cancelled);
1371 }
1372 let mut next: Vec<u64> = Vec::new();
1373 for o in &cur {
1374 if is_cancelled() {
1375 return Err(QueryError::Cancelled);
1376 }
1377 let edges: Vec<(String, u64, u64)> = match step.direction {
1378 Direction::Outgoing => access::adj_out(snap, tenant, *o, step.rel_type.as_deref())?,
1379 Direction::Incoming => access::adj_in(snap, tenant, *o, step.rel_type.as_deref())?,
1380 Direction::Both => {
1381 let mut all = access::adj_out(snap, tenant, *o, step.rel_type.as_deref())?;
1382 all.extend(access::adj_in(snap, tenant, *o, step.rel_type.as_deref())?);
1383 all
1384 }
1385 };
1386 for (_, other, _) in edges {
1387 if !step.target_labels.is_empty()
1388 && !access::record_by_ord(snap, tenant, other)?
1389 .is_some_and(|h| step.target_labels.contains(&h.label))
1390 {
1391 continue;
1392 }
1393 if let Some(pred) = &step.filter {
1394 let reader = aggregate_budget::RecordReader::new(
1395 snap,
1396 budget.map(|limit| {
1397 limit.saturating_sub(
1398 cur.len().saturating_add(next.len()).saturating_mul(32),
1399 )
1400 }),
1401 );
1402 let obj = filtered_row(&reader, snap, tenant, pred, other)?;
1403 if !eval_expr_ctx(&ctx, pred, &obj, other)? {
1404 continue;
1405 }
1406 }
1407 Budget::WorkingSet.check(
1408 budget,
1409 cur.len()
1410 .saturating_add(next.len())
1411 .saturating_add(1)
1412 .saturating_mul(32),
1413 )?;
1414 next.push(other);
1415 }
1416 }
1417 next.sort_unstable();
1418 next.dedup();
1419 cur = next;
1420 }
1421 Ok(cur)
1422}
1423
1424fn related_specs_from_where(expr: Option<&Expr>) -> Vec<RelatedSpec> {
1428 fn split_level(filter: Option<&Expr>, children: &mut Vec<Expr>) -> Option<Expr> {
1431 let f = filter?;
1432 match f {
1433 Expr::And(items) => {
1434 let mut preds: Vec<Expr> = Vec::new();
1435 for item in items {
1436 match item {
1437 Expr::Related(_) => children.push(item.clone()),
1438 other => preds.push(other.clone()),
1439 }
1440 }
1441 match preds.len() {
1442 0 => None,
1443 1 => Some(preds.remove(0)),
1444 _ => Some(Expr::And(preds)),
1445 }
1446 }
1447 Expr::Related(_) => {
1448 children.push(f.clone());
1449 None
1450 }
1451 other => Some(other.clone()),
1452 }
1453 }
1454
1455 fn walk(expr: &Expr, path: &mut Vec<TraversalStep>, out: &mut Vec<RelatedSpec>) {
1456 match expr {
1457 Expr::And(items) | Expr::Or(items) | Expr::Nor(items) => {
1458 for e in items {
1459 walk(e, path, out);
1460 }
1461 }
1462 Expr::Xor(a, b) => {
1463 walk(a, path, out);
1464 walk(b, path, out);
1465 }
1466 Expr::Not(b) => {
1467 walk(b, path, out);
1468 }
1469 Expr::Related(rel) => {
1470 let mut children: Vec<Expr> = Vec::new();
1471 let filter = split_level(rel.filter.as_deref(), &mut children);
1472 let step = TraversalStep {
1473 target_labels: rel.target_labels.clone(),
1474 rel_type: rel.rel_type.clone(),
1475 direction: rel.direction,
1476 filter,
1477 };
1478 path.push(step);
1479 if let Some(alias) = &rel.alias {
1480 out.push(RelatedSpec {
1481 alias: Some(alias.trim_start_matches('$').to_string()),
1482 rel_type: rel.rel_type.clone(),
1483 direction: rel.direction,
1484 target_labels: rel.target_labels.clone(),
1485 steps: path.clone(),
1486 });
1487 }
1488 for child in &children {
1489 walk(child, path, out);
1490 }
1491 path.pop();
1492 }
1493 Expr::Field(_) => {}
1494 }
1495 }
1496 let mut specs = Vec::new();
1497 if let Some(e) = expr {
1498 let mut path: Vec<TraversalStep> = Vec::new();
1499 walk(e, &mut path, &mut specs);
1500 }
1501 specs
1502}
1503
1504fn alias_field_values(
1508 lmpg: &Lmpg,
1509 snap: &QuerySnapshot,
1510 tenant: rushdb_types::ids::TenantId,
1511 ord: u64,
1512 specs: &[RelatedSpec],
1513 field: &str,
1514 budget: Option<usize>,
1515) -> Result<Option<Vec<Json>>, QueryError> {
1516 let Some((alias, prop)) = field.split_once('.') else {
1517 return Ok(None);
1518 };
1519 let Some(related) = count_related(lmpg, snap, tenant, ord, specs, alias, budget)? else {
1520 return Ok(None);
1521 };
1522 let mut retained = related.capacity().saturating_mul(8);
1523 Budget::WorkingSet.check(budget, retained)?;
1524 let mut values = Vec::new();
1525 let selected: std::collections::HashSet<String> =
1527 [prop.to_owned(), normalize(prop)].into_iter().collect();
1528 for other in related {
1529 let reader = aggregate_budget::RecordReader::new(
1530 snap,
1531 budget.map(|limit| limit.saturating_sub(retained)),
1532 );
1533 let mut session = reconstruct::ReconstructSession::new(&reader, tenant);
1534 if let Some(Json::Object(child)) = session.reconstruct_record_selected(other, &selected)? {
1535 if let Some(v) = resolve_field(&child, prop) {
1536 retained = retained.saturating_add(aggregate_budget::value_bytes(&v));
1537 Budget::WorkingSet.check(budget, retained)?;
1538 values.push(v);
1539 }
1540 }
1541 }
1542 Ok(Some(values))
1543}
1544
1545pub const MISSING_SENTINEL: &str = "__missing__";
1548
1549#[derive(Debug, Clone, Default, serde::Serialize)]
1552pub struct GroupBucket {
1553 pub keys: Map<String, Json>,
1556 pub count_roots: u64,
1558 pub sums: Map<String, Json>,
1560 pub counts: Map<String, Json>,
1562 pub mins: Map<String, Json>,
1564 pub maxs: Map<String, Json>,
1566 pub derived_outputs: std::collections::BTreeMap<String, Json>,
1568 #[serde(skip)]
1571 pub root: Option<u64>,
1572}
1573
1574#[derive(Debug, Clone)]
1577pub enum OutputExpr {
1578 Ref(String),
1580 Field(String),
1582 Add(Box<OutputExpr>, Box<OutputExpr>),
1584 Subtract(Box<OutputExpr>, Box<OutputExpr>),
1586 Multiply(Box<OutputExpr>, Box<OutputExpr>),
1588 Divide {
1590 dividend: Box<OutputExpr>,
1592 divisor: Box<OutputExpr>,
1594 precision: u32,
1596 },
1597 Literal(f64),
1599}
1600
1601#[derive(Debug, Clone, Default)]
1603pub struct AggregateRequest {
1604 pub labels: Vec<String>,
1606 pub where_: Option<Expr>,
1608 pub related: Vec<RelatedSpec>,
1610 pub group_by: Vec<String>,
1612 pub sums: Vec<(String, String)>,
1614 pub counts: Vec<(String, String)>,
1617 pub mins: Vec<(String, String)>,
1619 pub maxs: Vec<(String, String)>,
1621 pub time_bucket: Option<TimeBucketSpec>,
1623 pub outputs: Vec<(String, OutputExpr)>,
1625 pub expr_sums: Vec<(String, OutputExpr)>,
1627 pub expr_avgs: Vec<(String, OutputExpr)>,
1629 pub avg_terms: Vec<(String, String)>,
1631 pub avg_precision: std::collections::BTreeMap<String, u32>,
1633 pub time_bucket_alias: Option<String>,
1635 pub plain_fields: Vec<(String, String)>,
1637 pub collects: Vec<CollectSpec>,
1640 pub order_by: Vec<(String, bool )>,
1642 pub skip: u64,
1644 pub limit: Option<u64>,
1646 pub hidden_outputs: std::collections::BTreeSet<String>,
1648}
1649
1650pub struct EvalCtx<'a> {
1653 pub lmpg: &'a Lmpg,
1655 pub snap: QuerySnapshot,
1657 pub is_cancelled: Option<&'a dyn Fn() -> bool>,
1659 pub(crate) cycles: cycle::CycleGraphs,
1661 pub(crate) declared_types:
1665 std::cell::RefCell<std::collections::HashMap<String, Option<LogicalType>>>,
1666 pub(crate) record_types:
1670 std::cell::RefCell<std::collections::HashMap<String, Vec<(u64, LogicalType)>>>,
1671 adjacencies: std::cell::RefCell<AdjacencyByShape>,
1675}
1676
1677type AdjacencyByShape =
1678 std::collections::HashMap<(Option<String>, Direction), std::rc::Rc<adjacency::TypeAdjacency>>;
1679
1680impl<'a> EvalCtx<'a> {
1681 pub(crate) fn at(
1683 lmpg: &'a Lmpg,
1684 snap: QuerySnapshot,
1685 is_cancelled: Option<&'a dyn Fn() -> bool>,
1686 ) -> Self {
1687 EvalCtx {
1688 lmpg,
1689 snap,
1690 is_cancelled,
1691 cycles: cycle::CycleGraphs::default(),
1692 declared_types: std::cell::RefCell::default(),
1693 record_types: std::cell::RefCell::default(),
1694 adjacencies: std::cell::RefCell::default(),
1695 }
1696 }
1697
1698 fn adjacency(
1700 &self,
1701 rel_type: Option<&str>,
1702 direction: Direction,
1703 ) -> Result<std::rc::Rc<adjacency::TypeAdjacency>, QueryError> {
1704 let key = (rel_type.map(str::to_owned), direction);
1705 if let Some(open) = self.adjacencies.borrow().get(&key) {
1706 return Ok(std::rc::Rc::clone(open));
1707 }
1708 let cancel = || self.check_cancelled();
1709 let open = std::rc::Rc::new(adjacency::TypeAdjacency::open(
1710 &self.snap,
1711 self.lmpg.tenant,
1712 rel_type,
1713 direction,
1714 &cancel,
1715 )?);
1716 self.adjacencies
1717 .borrow_mut()
1718 .insert(key, std::rc::Rc::clone(&open));
1719 Ok(open)
1720 }
1721
1722 pub fn new(lmpg: &'a Lmpg) -> Result<Self, QueryError> {
1729 Ok(EvalCtx::at(lmpg, Arc::new(lmpg.store.read_view()?), None))
1730 }
1731
1732 pub fn check_cancelled(&self) -> Result<(), QueryError> {
1738 if self.is_cancelled.is_some_and(|probe| probe()) {
1739 Err(QueryError::Cancelled)
1740 } else {
1741 Ok(())
1742 }
1743 }
1744}
1745
1746fn eval_expr_ctx(
1749 ctx: &EvalCtx<'_>,
1750 expr: &Expr,
1751 obj: &Map<String, Json>,
1752 ord: u64,
1753) -> Result<bool, QueryError> {
1754 match expr {
1755 Expr::And(items) => {
1756 for item in items {
1757 if !eval_expr_ctx(ctx, item, obj, ord)? {
1758 return Ok(false);
1759 }
1760 }
1761 Ok(true)
1762 }
1763 Expr::Or(items) | Expr::Nor(items) => {
1764 let inverted = matches!(expr, Expr::Nor(_));
1765 for item in items {
1766 if eval_expr_ctx(ctx, item, obj, ord)? {
1767 return Ok(!inverted);
1768 }
1769 }
1770 Ok(inverted)
1771 }
1772 Expr::Xor(a, b) => Ok(eval_expr_ctx(ctx, a, obj, ord)? != eval_expr_ctx(ctx, b, obj, ord)?),
1773 Expr::Not(inner) => Ok(!eval_expr_ctx(ctx, inner, obj, ord)?),
1774 Expr::Field(pred) => eval_pred(&|name| type_held(ctx, name, ord), pred, obj),
1775 Expr::Related(rel) => expand::related_holds(ctx, ord, rel),
1776 }
1777}
1778
1779pub(crate) type DeclaredType<'a> =
1781 dyn Fn(&str) -> Result<Option<rushdb_types::model::LogicalType>, QueryError> + 'a;
1782
1783#[expect(
1784 clippy::too_many_lines,
1785 reason = "one exhaustive match over every field predicate keeps the operator semantics readable in a single place"
1786)]
1787pub(crate) fn eval_pred(
1788 declared_type: &DeclaredType<'_>,
1789 pred: &FieldPred,
1790 obj: &Map<String, Json>,
1791) -> Result<bool, QueryError> {
1792 Ok(match pred {
1793 FieldPred::Exists(name, want) => {
1794 let present = raw_field(obj, name).is_some();
1795 present == *want
1796 }
1797 FieldPred::IsNull(name, want) => {
1798 let v = raw_field(obj, name);
1799 let is_null = matches!(v, Some(Json::Null));
1800 is_null == *want
1801 }
1802 FieldPred::Eq(name, val) => match (raw_field(obj, name), val) {
1803 (Some(_), Json::Null) => matches!(raw_field(obj, name), Some(Json::Null)),
1804 (Some(actual), expected) => {
1805 let datetime = is_datetime(declared_type, name)?;
1806 any_element(actual, |el| value_equals(el, expected, datetime))
1807 }
1808 (None, _) => false,
1809 },
1810 FieldPred::Ne(name, val) => {
1811 match raw_field(obj, name) {
1813 None => false,
1814 Some(actual) => {
1815 let datetime = is_datetime(declared_type, name)?;
1816 !any_element(actual, |el| value_equals(el, val, datetime))
1817 }
1818 }
1819 }
1820 FieldPred::In(name, vals) => match raw_field(obj, name) {
1821 None => false,
1822 Some(actual) => {
1823 let datetime = is_datetime(declared_type, name)?;
1824 any_element(actual, |el| {
1825 vals.iter().any(|v| value_equals(el, v, datetime))
1826 })
1827 }
1828 },
1829 FieldPred::Contains(name, sub) => raw_field(obj, name).is_some_and(|j| {
1830 let sub = sub.to_lowercase();
1831 any_element(j, |el| {
1832 el.as_str()
1833 .is_some_and(|st| st.to_lowercase().contains(sub.as_str()))
1834 })
1835 }),
1836 FieldPred::StartsWith(name, prefix) => raw_field(obj, name).is_some_and(|j| {
1837 let prefix = prefix.to_lowercase();
1838 any_element(j, |el| {
1839 el.as_str()
1840 .is_some_and(|st| st.to_lowercase().starts_with(prefix.as_str()))
1841 })
1842 }),
1843 FieldPred::Gt(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1844 o == std::cmp::Ordering::Greater
1845 }),
1846 FieldPred::Gte(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1847 o != std::cmp::Ordering::Less
1848 }),
1849 FieldPred::Lt(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1850 o == std::cmp::Ordering::Less
1851 }),
1852 FieldPred::Lte(name, bound) => numeric_cmp_any(raw_field(obj, name), *bound, |o| {
1853 o != std::cmp::Ordering::Greater
1854 }),
1855 FieldPred::Nin(name, vals) => {
1856 match raw_field(obj, name) {
1858 None => false,
1859 Some(actual) => {
1860 let datetime = is_datetime(declared_type, name)?;
1861 !any_element(actual, |el| {
1862 vals.iter().any(|v| value_equals(el, v, datetime))
1863 })
1864 }
1865 }
1866 }
1867 FieldPred::EndsWith(name, suffix) => raw_field(obj, name).is_some_and(|j| {
1868 let suffix = suffix.to_lowercase();
1869 any_element(j, |el| {
1870 el.as_str()
1871 .is_some_and(|st| st.to_lowercase().ends_with(suffix.as_str()))
1872 })
1873 }),
1874 FieldPred::Id(id) => obj
1875 .get(ID_FIELD)
1876 .and_then(|v| v.as_str())
1877 .is_some_and(|s| canonical_id(s) == canonical_id(id)),
1878 FieldPred::DatetimeComponent {
1879 field,
1880 which,
1881 value,
1882 } => raw_field(obj, field)
1883 .and_then(serde_json::Value::as_i64)
1884 .is_some_and(|ms| {
1885 let actual = crate::datetime::component_of(ms, *which);
1886 actual == *value
1887 }),
1888 FieldPred::Type(name, expected) => {
1889 let want = canonical_type_alias(&expected.to_lowercase());
1890 let (base_want, want_array) = match want.strip_suffix("[]") {
1891 Some(base) if !base.is_empty() => (Some(canonical_type_alias(base)), true),
1892 _ => (None, want == "array"),
1893 };
1894 let value = raw_field(obj, name);
1895 if value.is_none() {
1898 return Ok(want == "null");
1899 }
1900 let is_array = matches!(value, Some(Json::Array(_)));
1901
1902 if want == "array" {
1904 return Ok(is_array);
1905 }
1906
1907 if let Some(ty) = declared_type(name)? {
1911 let name_of = |t: &rushdb_types::model::LogicalType| -> &'static str {
1912 match t {
1913 rushdb_types::model::LogicalType::Boolean => "boolean",
1914 rushdb_types::model::LogicalType::Int64
1915 | rushdb_types::model::LogicalType::Float64 => "number",
1916 rushdb_types::model::LogicalType::Utf8 => "string",
1917 rushdb_types::model::LogicalType::DateTime => "datetime",
1918 }
1919 };
1920 if want_array {
1921 if is_array && name_of(&ty) == base_want.as_deref().unwrap_or("") {
1924 return Ok(true);
1925 }
1926 } else if name_of(&ty) == want {
1927 return Ok(true);
1930 }
1931 }
1932
1933 let shape_of = |v: &Json| -> &'static str {
1934 if v.is_string() {
1935 "string"
1936 } else if v.is_number() {
1937 "number"
1938 } else if v.is_boolean() {
1939 "boolean"
1940 } else if v.is_array() {
1941 "array"
1942 } else if v.is_object() {
1943 "object"
1944 } else {
1945 "null"
1946 }
1947 };
1948
1949 if want_array {
1950 let base = base_want.as_deref().unwrap_or("");
1953 return Ok(
1954 matches!(value, Some(Json::Array(items)) if items.iter().any(|el| shape_of(el) == base)),
1955 );
1956 }
1957
1958 let actual = value.map_or("null", shape_of);
1959 actual == want
1960 }
1961 })
1962}
1963
1964fn numeric_cmp(
1965 value: Option<&Json>,
1966 bound: f64,
1967 accept: impl Fn(std::cmp::Ordering) -> bool,
1968) -> bool {
1969 value
1970 .and_then(infer_scalar)
1971 .and_then(|(_, scalar)| match scalar {
1972 Scalar::Int64(_) | Scalar::Float64(_) => Some(scalar),
1973 _ => None,
1974 })
1975 .and_then(|value| canonical_scalar_cmp(&value, &Scalar::Float64(bound)))
1976 .is_some_and(accept)
1977}
1978
1979fn numeric_cmp_any(
1980 value: Option<&Json>,
1981 bound: f64,
1982 accept: impl Fn(std::cmp::Ordering) -> bool,
1983) -> bool {
1984 match value {
1985 Some(v) if v.is_array() => v
1986 .as_array()
1987 .is_some_and(|a| a.iter().any(|el| numeric_cmp(Some(el), bound, &accept))),
1988 other => numeric_cmp(other, bound, accept),
1989 }
1990}
1991
1992fn canonical_type_alias(t: &str) -> String {
1996 if t == "bool" {
1997 "boolean".to_string()
1998 } else {
1999 t.to_string()
2000 }
2001}
2002
2003fn json_value_cmp(a: &Json, b: &Json) -> std::cmp::Ordering {
2008 let numeric = |v: &Json| {
2009 infer_scalar(v).and_then(|(_, scalar)| match scalar {
2010 Scalar::Int64(_) | Scalar::Float64(_) => Some(scalar),
2011 _ => None,
2012 })
2013 };
2014 match (numeric(a), numeric(b)) {
2015 (Some(x), Some(y)) => canonical_scalar_cmp(&x, &y).unwrap_or(std::cmp::Ordering::Equal),
2016 (None, Some(_)) => std::cmp::Ordering::Less,
2018 (Some(_), None) => std::cmp::Ordering::Greater,
2019 (None, None) => match (a.as_str(), b.as_str()) {
2020 (Some(x), Some(y)) => x.cmp(y),
2021 _ => format!("{a}").cmp(&format!("{b}")),
2022 },
2023 }
2024}
2025
2026fn raw_field<'a>(obj: &'a Map<String, Json>, name: &str) -> Option<&'a Json> {
2027 let norm = normalize(name);
2028 obj.iter()
2029 .find(|(k, _)| normalize(k) == norm)
2030 .map(|(_, v)| v)
2031}
2032
2033fn canonical_id(id: &str) -> String {
2034 id.bytes()
2035 .filter(|byte| *byte != b'-')
2036 .map(|byte| (byte as char).to_ascii_lowercase())
2037 .collect()
2038}
2039
2040#[must_use]
2042pub fn normalize(name: &str) -> String {
2043 rushdb_types::value::normalize_name(name)
2044}
2045
2046fn resolve_field(obj: &Map<String, Json>, field: &str) -> Option<Json> {
2047 let field = field.strip_prefix("$record.").unwrap_or(field);
2048 if let Some(value) = obj.get(field) {
2052 return Some(value.clone());
2053 }
2054 let norm = normalize(field);
2055 obj.iter()
2056 .find(|(k, _)| normalize(k) == norm)
2057 .map(|(_, v)| v.clone())
2058}
2059
2060#[cfg(test)]
2061mod index_planner_tests;