Skip to main content

nodedb_sql/optimizer/
point_get.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Detect equality on the primary key → convert Scan to PointGet.
4
5use nodedb_types::DatabaseId;
6
7use crate::catalog::SqlCatalog;
8use crate::types::*;
9
10/// If a Scan has a single equality filter on the collection's primary key,
11/// convert it to a PointGet.
12///
13/// The primary key is resolved from the catalog rather than assumed to be the
14/// conventional `id` / `document_id` / `key`: a collection created without an
15/// explicit `PRIMARY KEY` gets an auto-generated `_rowid` key, so a filter on a
16/// regular `id` column is NOT a point lookup and must stay a scan (routing it
17/// to PointGet would resolve a surrogate for the wrong key and return zero
18/// rows). When the catalog cannot resolve a primary key (unknown collection),
19/// fall back to the legacy convention so nothing regresses.
20pub fn optimize(plan: SqlPlan, catalog: &dyn SqlCatalog) -> SqlPlan {
21    match plan {
22        SqlPlan::Scan {
23            ref collection,
24            ref alias,
25            ref engine,
26            ref filters,
27            ref projection,
28            ref temporal,
29            ..
30        } if filters.len() == 1
31            && !temporal.is_temporal()
32            && !projection
33                .iter()
34                .any(|p| matches!(p, Projection::Computed { .. })) =>
35        {
36            let pk = catalog
37                .get_collection(DatabaseId::DEFAULT, collection)
38                .ok()
39                .flatten()
40                .and_then(|info| info.primary_key);
41            if let Some((key_col, key_val)) = extract_pk_equality(&filters[0], pk.as_deref()) {
42                return SqlPlan::PointGet {
43                    collection: collection.clone(),
44                    alias: alias.clone(),
45                    engine: *engine,
46                    key_column: key_col,
47                    key_value: key_val,
48                    projection: projection.clone(),
49                };
50            }
51            plan
52        }
53        _ => plan,
54    }
55}
56
57/// Extract a simple equality filter eligible for the point-get rewrite.
58///
59/// Only the conventional document-key columns (`id` / `document_id` / `key`)
60/// are candidates — this pass deliberately does not promote arbitrary declared
61/// primary keys (e.g. `sku`) to point lookups. The refinement over the legacy
62/// name-only check: when the catalog resolves a real primary key, a candidate
63/// column is eligible only if it actually IS that key. This excludes a regular
64/// `id` column on a collection whose real key is the auto-generated `_rowid`
65/// (where a point-get would resolve a surrogate for the wrong key and return
66/// zero rows) while leaving every previously-optimized case unchanged. When the
67/// catalog can't resolve a key, fall back to the name-only convention.
68fn extract_pk_equality(filter: &Filter, pk: Option<&str>) -> Option<(String, SqlValue)> {
69    let is_pk_column = |col: &str| -> bool {
70        let conventional = col == "id" || col == "document_id" || col == "key";
71        match pk {
72            Some(pk) => conventional && col.eq_ignore_ascii_case(pk),
73            None => conventional,
74        }
75    };
76    match &filter.expr {
77        FilterExpr::Comparison {
78            field,
79            op: CompareOp::Eq,
80            value,
81        } => {
82            let f = field.to_lowercase();
83            if is_pk_column(&f) {
84                Some((f, value.clone()))
85            } else {
86                None
87            }
88        }
89        FilterExpr::Expr(SqlExpr::BinaryOp {
90            left,
91            op: BinaryOp::Eq,
92            right,
93        }) => {
94            let col = match left.as_ref() {
95                SqlExpr::Column { name, .. } => name.to_lowercase(),
96                _ => return None,
97            };
98            if !is_pk_column(&col) {
99                return None;
100            }
101            let val = match right.as_ref() {
102                SqlExpr::Literal(v) => v.clone(),
103                _ => return None,
104            };
105            Some((col, val))
106        }
107        _ => None,
108    }
109}