Skip to main content

nodedb_lite/nodedb/core/ops/
indexed_crud.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Indexed CRUD for strict and columnar collections: combines base-engine
4//! writes with secondary-index maintenance (vector, spatial, text, B-tree)
5//! and HTAP materialized-view replication.
6
7use nodedb_types::error::{NodeDbError, NodeDbResult};
8
9use crate::nodedb::core::types::NodeDbLite;
10use crate::storage::engine::StorageEngine;
11
12impl<S: StorageEngine> NodeDbLite<S> {
13    // -- Indexed CRUD for strict/columnar collections --
14
15    /// Insert a row into a strict collection and update secondary indexes.
16    ///
17    /// Combines `StrictEngine.insert()` with `index_row()` for geometry,
18    /// vector, and text columns.
19    pub async fn strict_insert(
20        &self,
21        collection: &str,
22        values: &[nodedb_types::value::Value],
23    ) -> NodeDbResult<()> {
24        let guard = self.fts_state.admit_mutation().await;
25        let result = async {
26            let schema = self.strict.schema(collection).ok_or_else(|| {
27                NodeDbError::storage(format!("strict collection '{collection}' not found"))
28            })?;
29
30            // Insert into storage. `StrictEngine` is interior-mutable; await directly.
31            self.strict
32                .insert(collection, values)
33                .await
34                .map_err(NodeDbError::storage)?;
35
36            // Build a row_id string from the PK value for index keying.
37            let row_id = crate::engine::index_integration::row_id(&schema.columns, values);
38
39            // Update secondary indexes.
40            crate::engine::index_integration::index_row(
41                collection,
42                &row_id,
43                &schema.columns,
44                values,
45                &self.spatial,
46                &self.fts_state.manager,
47            )?;
48            crate::engine::index_integration::index_row_vectors(
49                &self.vector_state,
50                collection,
51                &row_id,
52                &schema.columns,
53                values,
54            )
55            .await?;
56
57            // Replicate to materialized columnar views (HTAP CDC).
58            self.htap
59                .replicate_insert(collection, values, &self.columnar);
60
61            Ok(())
62        }
63        .await;
64        guard.finish(result)
65    }
66
67    /// Delete a row from a strict collection and clean up text indexes.
68    pub async fn strict_delete(
69        &self,
70        collection: &str,
71        pk: &nodedb_types::value::Value,
72    ) -> NodeDbResult<bool> {
73        let guard = self.fts_state.admit_mutation().await;
74        let result = async {
75            let schema = self.strict.schema(collection).ok_or_else(|| {
76                NodeDbError::storage(format!("strict collection '{collection}' not found"))
77            })?;
78
79            // The same row id the insert indexed the row under.
80            let row_id = crate::engine::index_integration::pk_row_id(pk);
81
82            // Remove text and vector index entries before deleting the row.
83            crate::engine::index_integration::deindex_row_text(
84                collection,
85                &row_id,
86                &self.fts_state.manager,
87            )?;
88            crate::engine::index_integration::deindex_row_vectors(
89                &self.vector_state,
90                collection,
91                &row_id,
92                &schema.columns,
93            )
94            .await?;
95
96            // Replicate delete to materialized columnar views (HTAP CDC).
97            self.htap.replicate_delete(collection, pk, &self.columnar);
98
99            self.strict
100                .delete(collection, pk)
101                .await
102                .map_err(NodeDbError::storage)
103        }
104        .await;
105        guard.finish(result)
106    }
107
108    /// Insert a row into a columnar collection and update secondary indexes.
109    pub async fn columnar_insert(
110        &self,
111        collection: &str,
112        values: &[nodedb_types::value::Value],
113    ) -> NodeDbResult<()> {
114        let guard = self.fts_state.admit_mutation().await;
115        let result = async {
116            let schema = self.columnar.schema(collection).ok_or_else(|| {
117                NodeDbError::storage(format!("columnar collection '{collection}' not found"))
118            })?;
119
120            self.columnar
121                .insert(collection, values)
122                .map_err(NodeDbError::storage)?;
123
124            let row_id = crate::engine::index_integration::row_id(&schema.columns, values);
125
126            crate::engine::index_integration::index_row(
127                collection,
128                &row_id,
129                &schema.columns,
130                values,
131                &self.spatial,
132                &self.fts_state.manager,
133            )?;
134            crate::engine::index_integration::index_row_vectors(
135                &self.vector_state,
136                collection,
137                &row_id,
138                &schema.columns,
139                values,
140            )
141            .await?;
142
143            // Spatial profile: compute geohash for Point geometries and store
144            // in the text index for prefix-based proximity queries.
145            crate::engine::index_integration::index_geohash(
146                collection,
147                &row_id,
148                &schema,
149                self.columnar.profile(collection).as_ref(),
150                values,
151                &self.fts_state.manager,
152            )?;
153            Ok(())
154        }
155        .await;
156        guard.finish(result)
157    }
158
159    /// Apply a CRDT field-level update to a strict collection row.
160    ///
161    /// Used during sync: a remote delta specifies field changes for a row.
162    /// This reads the current tuple, patches the fields, and writes back.
163    pub async fn strict_crdt_patch(
164        &self,
165        collection: &str,
166        pk: &nodedb_types::value::Value,
167        field_updates: &std::collections::HashMap<String, nodedb_types::value::Value>,
168    ) -> NodeDbResult<()> {
169        let guard = self.fts_state.admit_mutation().await;
170        let result = async {
171            let schema = self.strict.schema(collection).ok_or_else(|| {
172                NodeDbError::storage(format!("strict collection '{collection}' not found"))
173            })?;
174
175            // Read existing tuple.
176            let existing = self
177                .strict
178                .get(collection, pk)
179                .await
180                .map_err(NodeDbError::storage)?
181                .ok_or_else(|| NodeDbError::storage("row not found for CRDT patch"))?;
182
183            // Re-encode as tuple bytes for the adapter.
184            let encoder = nodedb_strict::TupleEncoder::new(&schema);
185            let tuple_bytes = encoder
186                .encode(&existing)
187                .map_err(|e| NodeDbError::storage(e.to_string()))?;
188
189            // Apply the CRDT patch.
190            let patched = crate::engine::strict::crdt_adapter::apply_crdt_set(
191                &tuple_bytes,
192                &schema,
193                field_updates,
194            )
195            .map_err(NodeDbError::storage)?;
196
197            // Decode patched tuple back to values and update.
198            let decoder = nodedb_strict::TupleDecoder::new(&schema);
199            let new_values = decoder
200                .extract_all(&patched)
201                .map_err(|e| NodeDbError::storage(e.to_string()))?;
202
203            // Write back via the standard update path.
204            self.strict
205                .update_by_values(collection, pk, &new_values)
206                .await
207                .map_err(NodeDbError::storage)?;
208
209            // Re-index the patched row's text so no pre-patch term keeps matching.
210            crate::engine::index_integration::index_row_text(
211                collection,
212                &crate::engine::index_integration::row_id(&schema.columns, &new_values),
213                &schema.columns,
214                &new_values,
215                &self.fts_state.manager,
216            )?;
217
218            Ok(())
219        }
220        .await;
221        guard.finish(result)
222    }
223}