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}