Skip to main content

rushdb_query/
point_fields.rs

1//! Record properties read by point reads instead of rebuilding the record
2//!: the ordering properties of an ordered page, the properties a
3//! residual predicate names, and the columns a SQL label scan reads.
4
5use crate::planner::{family_id, pids_of};
6use crate::{access, normalize, reconstruct, Expr, FieldPred, Json, QueryError, QuerySnapshot};
7use rushdb_lmpg::reader::TryRead;
8use serde_json::Map;
9
10/// Record properties read with one point read per typed definition instead
11/// of rebuilding the record (READ-1: sorting 667K matches, or checking an
12/// unindexed `age < 25` on them, rebuilt every one). Covers scalar record
13/// properties only; [`Self::resolve`] answers `None` for a system field or a
14/// property stored as an array anywhere, and the caller rebuilds the record.
15pub struct PointFields(Vec<(String, Vec<u64>)>);
16
17impl PointFields {
18    /// Point reads for `names`, each row keyed by the name as given; `None`
19    /// when one of them needs the whole record.
20    ///
21    /// # Errors
22    /// Returns storage read errors from the property catalog.
23    pub fn resolve<'n>(
24        snap: &QuerySnapshot,
25        tenant: rushdb_types::ids::TenantId,
26        names: impl IntoIterator<Item = &'n str>,
27    ) -> Result<Option<Self>, QueryError> {
28        let mut fields = Vec::new();
29        for name in names {
30            let Some(pids) = scalar_pids(snap, tenant, name)? else {
31                return Ok(None);
32            };
33            fields.push((name.to_owned(), pids));
34        }
35        Ok(Some(Self(fields)))
36    }
37
38    /// The properties `predicate` reads, or `None` when it needs the whole
39    /// record.
40    pub(super) fn for_predicate(
41        snap: &QuerySnapshot,
42        tenant: rushdb_types::ids::TenantId,
43        predicate: &Expr,
44    ) -> Result<Option<Self>, QueryError> {
45        let mut names = std::collections::BTreeSet::new();
46        if !predicate_fields(predicate, &mut names) {
47            return Ok(None);
48        }
49        Self::resolve(snap, tenant, names.iter().map(String::as_str))
50    }
51
52    /// The fields of record `ord`: each one's value, as the rebuilt record
53    /// would carry it (the highest typed definition holding
54    /// one wins, as when occurrences fold into a record).
55    ///
56    /// # Errors
57    /// Returns storage read errors and corrupt-occurrence errors.
58    pub fn row(
59        &self,
60        snap: &QuerySnapshot,
61        tenant: rushdb_types::ids::TenantId,
62        ord: u64,
63    ) -> Result<Map<String, Json>, QueryError> {
64        let mut row = Map::new();
65        for (field, pids) in &self.0 {
66            // Highest first: the first definition holding a value wins.
67            for &pid in pids.iter().rev() {
68                if let Some(occ) = access::occurrence(
69                    snap,
70                    tenant,
71                    rushdb_types::ids::OwnerKind::Record,
72                    ord,
73                    pid,
74                    0,
75                )? {
76                    row.insert(
77                        field.clone(),
78                        reconstruct::decode_value(occ.value, &occ.payload)?,
79                    );
80                    break;
81                }
82            }
83        }
84        Ok(row)
85    }
86}
87
88/// Collect the property names `expr` reads into `out`; `false` when it needs
89/// the whole record (`$id`, traversals).
90fn predicate_fields(expr: &Expr, out: &mut std::collections::BTreeSet<String>) -> bool {
91    match expr {
92        Expr::And(items) | Expr::Or(items) | Expr::Nor(items) => {
93            items.iter().all(|item| predicate_fields(item, out))
94        }
95        Expr::Xor(a, b) => predicate_fields(a, out) && predicate_fields(b, out),
96        Expr::Not(inner) => predicate_fields(inner, out),
97        Expr::Related(_) | Expr::Field(FieldPred::Id(_)) => false,
98        Expr::Field(
99            FieldPred::Eq(name, _)
100            | FieldPred::Ne(name, _)
101            | FieldPred::Gt(name, _)
102            | FieldPred::Gte(name, _)
103            | FieldPred::Lt(name, _)
104            | FieldPred::Lte(name, _)
105            | FieldPred::Contains(name, _)
106            | FieldPred::StartsWith(name, _)
107            | FieldPred::EndsWith(name, _)
108            | FieldPred::Exists(name, _)
109            | FieldPred::IsNull(name, _)
110            | FieldPred::In(name, _)
111            | FieldPred::Nin(name, _)
112            | FieldPred::Type(name, _)
113            | FieldPred::DatetimeComponent { field: name, .. },
114        ) => {
115            out.insert(name.clone());
116            true
117        }
118    }
119}
120
121/// Typed definitions holding record property `name` as a scalar, ascending,
122/// for reading its value with point reads. `None` when the name is a system
123/// field or any label stores the property as an array: those need the whole
124/// record.
125fn scalar_pids<R: TryRead>(
126    snap: &R,
127    tenant: rushdb_types::ids::TenantId,
128    name: &str,
129) -> Result<Option<Vec<u64>>, QueryError> {
130    let norm = normalize(name);
131    if norm.is_empty() || rushdb_types::value::is_reserved_name(&norm) {
132        return Ok(None);
133    }
134    let Some(fid) = family_id(snap, tenant, &norm)? else {
135        return Ok(Some(Vec::new()));
136    };
137    let mut pids = pids_of(snap, tenant, fid)?;
138    for &pid in &pids {
139        let (arrays, _) = rushdb_lmpg::derived::array_markers(
140            snap,
141            tenant,
142            rushdb_types::ids::OwnerKind::Record,
143            pid,
144        )?;
145        if arrays {
146            return Ok(None);
147        }
148    }
149    pids.sort_unstable();
150    Ok(Some(pids))
151}