Skip to main content

nodedb_lite/engine/columnar/
store.rs

1//! Columnar engine for Lite: manages per-collection memtables, segments,
2//! delete bitmaps, and PK indexes against the StorageEngine.
3//!
4//! Segments are stored in the `Columnar` namespace as:
5//! - `{collection}:seg:{segment_id}` — segment bytes
6//! - `{collection}:del:{segment_id}` — delete bitmap bytes
7//! - `{collection}:meta` — segment metadata (list of segment IDs + row counts)
8//!
9//! Schemas are stored in the `Meta` namespace as `columnar_schema:{collection}`.
10
11use std::collections::HashMap;
12use std::sync::Arc;
13
14use nodedb_columnar::delete_bitmap::DeleteBitmap;
15use nodedb_columnar::mutation::MutationEngine;
16use nodedb_columnar::reader::SegmentReader;
17use nodedb_columnar::writer::SegmentWriter;
18use nodedb_types::Namespace;
19use nodedb_types::columnar::{ColumnarProfile, ColumnarSchema};
20use nodedb_types::value::Value;
21
22use crate::error::LiteError;
23use crate::storage::engine::{StorageEngine, WriteOp};
24
25/// Meta key prefix for columnar schemas.
26const META_COLUMNAR_SCHEMA_PREFIX: &str = "columnar_schema:";
27/// Meta key listing all columnar collections.
28const META_COLUMNAR_COLLECTIONS: &[u8] = b"meta:columnar_collections";
29
30/// Per-collection segment metadata.
31#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
32struct SegmentMeta {
33    segment_id: u32,
34    row_count: u64,
35}
36
37/// Per-collection state.
38struct CollectionState {
39    mutation: MutationEngine,
40    profile: ColumnarProfile,
41    /// Ordered list of flushed segments.
42    segments: Vec<SegmentMeta>,
43    /// Next segment ID to assign.
44    next_segment_id: u32,
45}
46
47/// Manages all columnar collections for a NodeDbLite instance.
48pub struct ColumnarEngine<S: StorageEngine> {
49    storage: Arc<S>,
50    collections: HashMap<String, CollectionState>,
51}
52
53impl<S: StorageEngine> ColumnarEngine<S> {
54    /// Create a new empty columnar engine.
55    pub fn new(storage: Arc<S>) -> Self {
56        Self {
57            storage,
58            collections: HashMap::new(),
59        }
60    }
61
62    /// Restore columnar collections from storage on startup.
63    pub async fn restore(storage: Arc<S>) -> Result<Self, LiteError> {
64        let mut engine = Self::new(Arc::clone(&storage));
65
66        let list_bytes = storage
67            .get(Namespace::Meta, META_COLUMNAR_COLLECTIONS)
68            .await?;
69        let names: Vec<String> = match list_bytes {
70            Some(bytes) => rmp_serde::from_slice(&bytes).map_err(|e| LiteError::Storage {
71                detail: format!("columnar collection list deserialization: {e}"),
72            })?,
73            None => Vec::new(),
74        };
75
76        for name in names {
77            let meta_key = format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}");
78            #[derive(serde::Deserialize)]
79            struct StoredSchema {
80                schema: ColumnarSchema,
81                profile: ColumnarProfile,
82            }
83            if let Some(schema_bytes) = storage.get(Namespace::Meta, meta_key.as_bytes()).await?
84                && let Ok(stored) = rmp_serde::from_slice::<StoredSchema>(&schema_bytes)
85            {
86                // Restore segment metadata.
87                let seg_meta_key = format!("{name}:meta");
88                let segments: Vec<SegmentMeta> = storage
89                    .get(Namespace::Columnar, seg_meta_key.as_bytes())
90                    .await?
91                    .and_then(|b| rmp_serde::from_slice(&b).ok())
92                    .unwrap_or_default();
93
94                let next_id = segments.iter().map(|s| s.segment_id + 1).max().unwrap_or(1);
95
96                // Restore PK index from segments.
97                let mut mutation = MutationEngine::new(name.clone(), stored.schema.clone());
98
99                // Rebuild PK index by scanning segment PKs.
100                // For now, PK index is rebuilt from segment metadata on cold start.
101                // A checkpoint-based approach would be faster for large datasets.
102                for seg_meta in &segments {
103                    let seg_key = format!("{name}:seg:{}", seg_meta.segment_id);
104                    if let Some(seg_bytes) =
105                        storage.get(Namespace::Columnar, seg_key.as_bytes()).await?
106                        && let Ok(reader) = SegmentReader::open(&seg_bytes)
107                    {
108                        // Read PK column (column 0 by convention for columnar collections).
109                        if let Ok(pk_col) = reader.read_column(0) {
110                            rebuild_pk_from_column(&mut mutation, &pk_col, seg_meta.segment_id);
111                        }
112                    }
113
114                    // Restore delete bitmap.
115                    let del_key = format!("{name}:del:{}", seg_meta.segment_id);
116                    if let Some(del_bytes) =
117                        storage.get(Namespace::Columnar, del_key.as_bytes()).await?
118                        && let Ok(bitmap) = DeleteBitmap::from_bytes(&del_bytes)
119                    {
120                        // Apply deletions to PK index.
121                        for row_idx in bitmap.iter() {
122                            // We don't have PK values for deleted rows in the bitmap,
123                            // so we just note the bitmap exists. The MutationEngine's
124                            // delete_bitmaps will be populated on the next delete op.
125                            let _ = row_idx;
126                        }
127                    }
128                }
129
130                engine.collections.insert(
131                    name,
132                    CollectionState {
133                        mutation,
134                        profile: stored.profile,
135                        segments,
136                        next_segment_id: next_id,
137                    },
138                );
139            }
140        }
141
142        Ok(engine)
143    }
144
145    // -- Schema management --
146
147    /// Create a new columnar collection.
148    pub async fn create_collection(
149        &mut self,
150        name: &str,
151        schema: ColumnarSchema,
152        profile: ColumnarProfile,
153    ) -> Result<(), LiteError> {
154        if self.collections.contains_key(name) {
155            return Err(LiteError::BadRequest {
156                detail: format!("columnar collection '{name}' already exists"),
157            });
158        }
159
160        // Persist schema + profile.
161        #[derive(serde::Serialize)]
162        struct StoredSchema<'a> {
163            schema: &'a ColumnarSchema,
164            profile: &'a ColumnarProfile,
165        }
166        let meta_key = format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}");
167        let schema_bytes = rmp_serde::to_vec_named(&StoredSchema {
168            schema: &schema,
169            profile: &profile,
170        })
171        .map_err(|e| LiteError::Serialization {
172            detail: e.to_string(),
173        })?;
174
175        let mut names: Vec<String> = self.collections.keys().cloned().collect();
176        names.push(name.to_string());
177        let names_bytes =
178            rmp_serde::to_vec_named(&names).map_err(|e| LiteError::Serialization {
179                detail: e.to_string(),
180            })?;
181
182        self.storage
183            .batch_write(&[
184                WriteOp::Put {
185                    ns: Namespace::Meta,
186                    key: meta_key.into_bytes(),
187                    value: schema_bytes,
188                },
189                WriteOp::Put {
190                    ns: Namespace::Meta,
191                    key: META_COLUMNAR_COLLECTIONS.to_vec(),
192                    value: names_bytes,
193                },
194            ])
195            .await?;
196
197        let mutation = MutationEngine::new(name.to_string(), schema);
198        self.collections.insert(
199            name.to_string(),
200            CollectionState {
201                mutation,
202                profile,
203                segments: Vec::new(),
204                next_segment_id: 1,
205            },
206        );
207
208        Ok(())
209    }
210
211    /// Drop a columnar collection and all its data.
212    pub async fn drop_collection(&mut self, name: &str) -> Result<(), LiteError> {
213        let state = self.collections.remove(name).ok_or(LiteError::BadRequest {
214            detail: format!("columnar collection '{name}' does not exist"),
215        })?;
216
217        let mut ops = Vec::new();
218
219        // Delete all segments and delete bitmaps.
220        for seg in &state.segments {
221            ops.push(WriteOp::Delete {
222                ns: Namespace::Columnar,
223                key: format!("{name}:seg:{}", seg.segment_id).into_bytes(),
224            });
225            ops.push(WriteOp::Delete {
226                ns: Namespace::Columnar,
227                key: format!("{name}:del:{}", seg.segment_id).into_bytes(),
228            });
229        }
230
231        // Delete metadata.
232        ops.push(WriteOp::Delete {
233            ns: Namespace::Columnar,
234            key: format!("{name}:meta").into_bytes(),
235        });
236        ops.push(WriteOp::Delete {
237            ns: Namespace::Meta,
238            key: format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}").into_bytes(),
239        });
240
241        // Update collection list.
242        let names: Vec<String> = self.collections.keys().cloned().collect();
243        let names_bytes =
244            rmp_serde::to_vec_named(&names).map_err(|e| LiteError::Serialization {
245                detail: e.to_string(),
246            })?;
247        ops.push(WriteOp::Put {
248            ns: Namespace::Meta,
249            key: META_COLUMNAR_COLLECTIONS.to_vec(),
250            value: names_bytes,
251        });
252
253        self.storage.batch_write(&ops).await?;
254        Ok(())
255    }
256
257    /// Add a column to an existing columnar collection.
258    ///
259    /// Bumps the schema version. Existing segments are NOT rewritten — the
260    /// reader null-fills columns that were added after the segment was written.
261    pub async fn alter_add_column(
262        &mut self,
263        name: &str,
264        column: nodedb_types::columnar::ColumnDef,
265    ) -> Result<(), LiteError> {
266        let state = self
267            .collections
268            .get_mut(name)
269            .ok_or(LiteError::BadRequest {
270                detail: format!("columnar collection '{name}' does not exist"),
271            })?;
272
273        // Non-nullable columns without a default break existing segments.
274        if !column.nullable && column.default.is_none() {
275            return Err(LiteError::BadRequest {
276                detail: format!(
277                    "ALTER ADD COLUMN '{}': non-nullable column must have a DEFAULT",
278                    column.name
279                ),
280            });
281        }
282
283        // Check for duplicate.
284        if state
285            .mutation
286            .schema()
287            .columns
288            .iter()
289            .any(|c| c.name == column.name)
290        {
291            return Err(LiteError::BadRequest {
292                detail: format!("column '{}' already exists in '{name}'", column.name),
293            });
294        }
295
296        // The MutationEngine owns the schema — we need to rebuild it with the new column.
297        // Extract current schema, append column, bump version, create new engine.
298        let mut schema = state.mutation.schema().clone();
299        schema.columns.push(column);
300        schema.version = schema.version.saturating_add(1);
301
302        // Rebuild the mutation engine with the new schema.
303        // PK index and delete bitmaps are preserved since column addition doesn't change row layout.
304        state.mutation = MutationEngine::new(name.to_string(), schema.clone());
305
306        // Persist updated schema + profile.
307        #[derive(serde::Serialize)]
308        struct StoredSchema<'a> {
309            schema: &'a ColumnarSchema,
310            profile: &'a ColumnarProfile,
311        }
312        let meta_key = format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}");
313        let schema_bytes = rmp_serde::to_vec_named(&StoredSchema {
314            schema: &schema,
315            profile: &state.profile,
316        })
317        .map_err(|e| LiteError::Serialization {
318            detail: e.to_string(),
319        })?;
320
321        self.storage
322            .put(Namespace::Meta, meta_key.as_bytes(), &schema_bytes)
323            .await?;
324
325        Ok(())
326    }
327
328    /// Get the schema for a collection.
329    pub fn schema(&self, name: &str) -> Option<&ColumnarSchema> {
330        self.collections.get(name).map(|s| s.mutation.schema())
331    }
332
333    /// Get the profile for a collection.
334    pub fn profile(&self, name: &str) -> Option<&ColumnarProfile> {
335        self.collections.get(name).map(|s| &s.profile)
336    }
337
338    /// List all columnar collection names.
339    pub fn collection_names(&self) -> Vec<&str> {
340        self.collections.keys().map(|s| s.as_str()).collect()
341    }
342
343    // -- Write path --
344
345    /// Insert a row into a columnar collection's memtable.
346    pub fn insert(&mut self, collection: &str, values: &[Value]) -> Result<(), LiteError> {
347        let state = self.get_state_mut(collection)?;
348        state
349            .mutation
350            .insert(values)
351            .map_err(columnar_err_to_lite)?;
352        Ok(())
353    }
354
355    /// Delete a row by PK.
356    ///
357    /// Rejects deletion on timeseries collections (append-only constraint).
358    /// Marks the row in the segment's delete bitmap. Compaction is triggered
359    /// separately via `try_compact_collection` when the delete ratio exceeds 20%.
360    pub fn delete(&mut self, collection: &str, pk: &Value) -> Result<bool, LiteError> {
361        let state = self.get_state_mut(collection)?;
362
363        // Timeseries profile is append-only — DELETE not allowed.
364        if matches!(state.profile, ColumnarProfile::Timeseries { .. }) {
365            return Err(LiteError::BadRequest {
366                detail: format!(
367                    "DELETE not allowed on timeseries collection '{collection}' (append-only)"
368                ),
369            });
370        }
371
372        match state.mutation.delete(pk) {
373            Ok(_) => Ok(true),
374            Err(nodedb_columnar::ColumnarError::PrimaryKeyNotFound) => Ok(false),
375            Err(e) => Err(columnar_err_to_lite(e)),
376        }
377    }
378
379    /// Update a row: DELETE old + INSERT new.
380    ///
381    /// Rejects update on timeseries collections (append-only constraint).
382    pub fn update(
383        &mut self,
384        collection: &str,
385        old_pk: &Value,
386        new_values: &[Value],
387    ) -> Result<bool, LiteError> {
388        let state = self.get_state_mut(collection)?;
389
390        // Timeseries profile is append-only — UPDATE not allowed.
391        if matches!(state.profile, ColumnarProfile::Timeseries { .. }) {
392            return Err(LiteError::BadRequest {
393                detail: format!(
394                    "UPDATE not allowed on timeseries collection '{collection}' (append-only)"
395                ),
396            });
397        }
398
399        match state.mutation.update(old_pk, new_values) {
400            Ok(_) => Ok(true),
401            Err(nodedb_columnar::ColumnarError::PrimaryKeyNotFound) => Ok(false),
402            Err(e) => Err(columnar_err_to_lite(e)),
403        }
404    }
405
406    /// Flush the memtable for a collection to a new segment.
407    ///
408    /// Called when the memtable reaches its threshold or on shutdown.
409    pub async fn flush_collection(&mut self, collection: &str) -> Result<(), LiteError> {
410        let state = self
411            .collections
412            .get_mut(collection)
413            .ok_or(LiteError::BadRequest {
414                detail: format!("columnar collection '{collection}' does not exist"),
415            })?;
416
417        if state.mutation.memtable().is_empty() {
418            return Ok(());
419        }
420
421        let segment_id = state.next_segment_id;
422        state.next_segment_id += 1;
423
424        // Drain the memtable and write a segment.
425        let (schema, columns, row_count) = state.mutation.memtable_mut().drain();
426
427        let profile_tag = match &state.profile {
428            ColumnarProfile::Plain => 0,
429            ColumnarProfile::Timeseries { .. } => 1,
430            ColumnarProfile::Spatial { .. } => 2,
431        };
432
433        let writer = SegmentWriter::new(profile_tag);
434        let segment_bytes = writer
435            .write_segment(&schema, &columns, row_count)
436            .map_err(columnar_err_to_lite)?;
437
438        // Collect all storage ops to execute after releasing mutable borrow on state.
439        let seg_key = format!("{collection}:seg:{segment_id}");
440        state.segments.push(SegmentMeta {
441            segment_id,
442            row_count: row_count as u64,
443        });
444        let meta_key = format!("{collection}:meta");
445        let meta_bytes =
446            rmp_serde::to_vec_named(&state.segments).map_err(|e| LiteError::Serialization {
447                detail: e.to_string(),
448            })?;
449
450        state.mutation.on_memtable_flushed(segment_id);
451
452        // Collect delete bitmap writes.
453        let mut del_ops: Vec<(String, Vec<u8>)> = Vec::new();
454        for (&seg_id, bitmap) in state.mutation.delete_bitmaps() {
455            if !bitmap.is_empty() {
456                let del_key = format!("{collection}:del:{seg_id}");
457                let del_bytes = bitmap.to_bytes().map_err(columnar_err_to_lite)?;
458                del_ops.push((del_key, del_bytes));
459            }
460        }
461
462        // Now do all storage writes (state borrow is released by building the ops above).
463        let storage = &self.storage;
464        storage
465            .put(Namespace::Columnar, seg_key.as_bytes(), &segment_bytes)
466            .await?;
467        storage
468            .put(Namespace::Columnar, meta_key.as_bytes(), &meta_bytes)
469            .await?;
470        for (del_key, del_bytes) in &del_ops {
471            storage
472                .put(Namespace::Columnar, del_key.as_bytes(), del_bytes)
473                .await?;
474        }
475
476        Ok(())
477    }
478
479    /// Flush all collections' memtables.
480    pub async fn flush_all(&mut self) -> Result<(), LiteError> {
481        let names: Vec<String> = self.collections.keys().cloned().collect();
482        for name in names {
483            self.flush_collection(&name).await?;
484        }
485        Ok(())
486    }
487
488    // -- Compaction --
489
490    /// Check if any segments need compaction and run it.
491    ///
492    /// Compaction is triggered when a segment's delete ratio exceeds 20%.
493    /// The old segment is replaced with a compacted one (deleted rows removed).
494    pub async fn try_compact_collection(&mut self, collection: &str) -> Result<bool, LiteError> {
495        let state = self
496            .collections
497            .get(collection)
498            .ok_or(LiteError::BadRequest {
499                detail: format!("columnar collection '{collection}' does not exist"),
500            })?;
501
502        // Find segments needing compaction.
503        let mut to_compact = Vec::new();
504        for seg_meta in &state.segments {
505            if let Some(bitmap) = state.mutation.delete_bitmap(seg_meta.segment_id)
506                && bitmap.should_compact(seg_meta.row_count, 0.2)
507            {
508                to_compact.push(seg_meta.segment_id);
509            }
510        }
511
512        if to_compact.is_empty() {
513            return Ok(false);
514        }
515
516        let schema = state.mutation.schema().clone();
517        let profile_tag = match &state.profile {
518            ColumnarProfile::Plain => 0,
519            ColumnarProfile::Timeseries { .. } => 1,
520            ColumnarProfile::Spatial { .. } => 2,
521        };
522
523        // Compact each segment that exceeds the threshold.
524        for seg_id in &to_compact {
525            let seg_key = format!("{collection}:seg:{seg_id}");
526            let seg_bytes = match self
527                .storage
528                .get(Namespace::Columnar, seg_key.as_bytes())
529                .await?
530            {
531                Some(b) => b,
532                None => continue,
533            };
534
535            let empty_bitmap = DeleteBitmap::new();
536            let bitmap = self
537                .collections
538                .get(collection)
539                .and_then(|s| s.mutation.delete_bitmap(*seg_id))
540                .unwrap_or(&empty_bitmap);
541
542            let result = nodedb_columnar::compaction::compact_segment(
543                &seg_bytes,
544                bitmap,
545                &schema,
546                profile_tag,
547            )
548            .map_err(columnar_err_to_lite)?;
549
550            if let Some(new_seg_bytes) = result.segment {
551                // Write compacted segment under the same key (atomic replace).
552                self.storage
553                    .put(Namespace::Columnar, seg_key.as_bytes(), &new_seg_bytes)
554                    .await?;
555
556                // Update row count in metadata.
557                if let Some(state) = self.collections.get_mut(collection)
558                    && let Some(meta) = state.segments.iter_mut().find(|m| m.segment_id == *seg_id)
559                {
560                    meta.row_count = result.live_rows as u64;
561                }
562
563                // Clear the delete bitmap for this segment.
564                let del_key = format!("{collection}:del:{seg_id}");
565                self.storage
566                    .delete(Namespace::Columnar, del_key.as_bytes())
567                    .await?;
568            } else {
569                // All rows deleted — remove segment entirely.
570                self.storage
571                    .delete(Namespace::Columnar, seg_key.as_bytes())
572                    .await?;
573                let del_key = format!("{collection}:del:{seg_id}");
574                self.storage
575                    .delete(Namespace::Columnar, del_key.as_bytes())
576                    .await?;
577
578                if let Some(state) = self.collections.get_mut(collection) {
579                    state.segments.retain(|m| m.segment_id != *seg_id);
580                }
581            }
582        }
583
584        // Persist updated metadata.
585        if let Some(state) = self.collections.get(collection) {
586            let meta_key = format!("{collection}:meta");
587            let meta_bytes =
588                rmp_serde::to_vec_named(&state.segments).map_err(|e| LiteError::Serialization {
589                    detail: e.to_string(),
590                })?;
591            self.storage
592                .put(Namespace::Columnar, meta_key.as_bytes(), &meta_bytes)
593                .await?;
594        }
595
596        Ok(true)
597    }
598
599    // -- Read path --
600
601    /// Read all segment bytes for a collection (for the table provider).
602    pub async fn read_segments(&self, collection: &str) -> Result<Vec<(u32, Vec<u8>)>, LiteError> {
603        let state = self.get_state(collection)?;
604        let mut segments = Vec::with_capacity(state.segments.len());
605
606        for seg_meta in &state.segments {
607            let seg_key = format!("{collection}:seg:{}", seg_meta.segment_id);
608            if let Some(bytes) = self
609                .storage
610                .get(Namespace::Columnar, seg_key.as_bytes())
611                .await?
612            {
613                segments.push((seg_meta.segment_id, bytes));
614            }
615        }
616
617        Ok(segments)
618    }
619
620    /// Get the delete bitmap for a segment.
621    pub fn delete_bitmap(&self, collection: &str, segment_id: u32) -> Option<&DeleteBitmap> {
622        self.collections
623            .get(collection)
624            .and_then(|s| s.mutation.delete_bitmap(segment_id))
625    }
626
627    /// Row count across all segments + memtable for a collection.
628    pub fn row_count(&self, collection: &str) -> usize {
629        let Some(state) = self.collections.get(collection) else {
630            return 0;
631        };
632        let seg_rows: u64 = state.segments.iter().map(|s| s.row_count).sum();
633        seg_rows as usize + state.mutation.memtable().row_count()
634    }
635
636    // -- Internal helpers --
637
638    fn get_state(&self, collection: &str) -> Result<&CollectionState, LiteError> {
639        self.collections
640            .get(collection)
641            .ok_or(LiteError::BadRequest {
642                detail: format!("columnar collection '{collection}' does not exist"),
643            })
644    }
645
646    fn get_state_mut(&mut self, collection: &str) -> Result<&mut CollectionState, LiteError> {
647        self.collections
648            .get_mut(collection)
649            .ok_or(LiteError::BadRequest {
650                detail: format!("columnar collection '{collection}' does not exist"),
651            })
652    }
653}
654
655/// Rebuild PK index entries from a decoded PK column.
656fn rebuild_pk_from_column(
657    mutation: &mut MutationEngine,
658    pk_col: &nodedb_columnar::reader::DecodedColumn,
659    segment_id: u32,
660) {
661    use nodedb_columnar::pk_index::{RowLocation, encode_pk};
662    use nodedb_columnar::reader::DecodedColumn;
663
664    match pk_col {
665        DecodedColumn::Int64 { values, valid } => {
666            for (row_idx, (val, &is_valid)) in values.iter().zip(valid.iter()).enumerate() {
667                if is_valid {
668                    let pk_bytes = encode_pk(&Value::Integer(*val));
669                    mutation.pk_index_mut().upsert(
670                        pk_bytes,
671                        RowLocation {
672                            segment_id,
673                            row_index: row_idx as u32,
674                        },
675                    );
676                }
677            }
678        }
679        DecodedColumn::Binary {
680            data,
681            offsets,
682            valid,
683        } => {
684            for (row_idx, &is_valid) in valid.iter().enumerate() {
685                if is_valid {
686                    let start = offsets[row_idx] as usize;
687                    let end = offsets[row_idx + 1] as usize;
688                    let pk_bytes = data[start..end].to_vec();
689                    mutation.pk_index_mut().upsert(
690                        pk_bytes,
691                        RowLocation {
692                            segment_id,
693                            row_index: row_idx as u32,
694                        },
695                    );
696                }
697            }
698        }
699        _ => {}
700    }
701}
702
703fn columnar_err_to_lite(e: nodedb_columnar::ColumnarError) -> LiteError {
704    LiteError::BadRequest {
705        detail: e.to_string(),
706    }
707}