Skip to main content

nodedb_lite/engine/columnar/store/
schema.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Collection schema creation, alteration, and removal.
4
5use super::lifecycle::{META_COLUMNAR_COLLECTIONS, META_COLUMNAR_SCHEMA_PREFIX};
6use super::segments::remove_segment_bytes;
7use super::state::{CollectionState, ColumnarEngine, SegmentMeta};
8use crate::error::LiteError;
9use crate::storage::engine::{StorageEngine, WriteOp};
10use nodedb_columnar::mutation::MutationEngine;
11use nodedb_types::Namespace;
12use nodedb_types::columnar::{ColumnarProfile, ColumnarSchema};
13use std::sync::{Arc, Mutex};
14
15impl<S: StorageEngine> ColumnarEngine<S> {
16    /// Create a new columnar collection.
17    pub async fn create_collection(
18        &self,
19        name: &str,
20        schema: ColumnarSchema,
21        profile: ColumnarProfile,
22        bitemporal: bool,
23    ) -> Result<(), LiteError> {
24        // Snapshot existing names + dup check under read lock.
25        let mut names: Vec<String> = {
26            let guard = self
27                .collections
28                .read()
29                .map_err(|_| LiteError::LockPoisoned)?;
30            if guard.contains_key(name) {
31                return Err(LiteError::BadRequest {
32                    detail: format!("columnar collection '{name}' already exists"),
33                });
34            }
35            guard.keys().cloned().collect()
36        };
37        names.push(name.to_string());
38
39        // Persist schema + collection list (lock dropped).
40        #[derive(serde::Serialize, zerompk::ToMessagePack)]
41        struct StoredSchema<'a> {
42            schema: &'a ColumnarSchema,
43            profile: &'a ColumnarProfile,
44            bitemporal: bool,
45        }
46        let meta_key = format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}");
47        let schema_bytes = zerompk::to_msgpack_vec(&StoredSchema {
48            schema: &schema,
49            profile: &profile,
50            bitemporal,
51        })
52        .map_err(|e| LiteError::Serialization {
53            detail: e.to_string(),
54        })?;
55
56        let names_bytes =
57            zerompk::to_msgpack_vec(&names).map_err(|e| LiteError::Serialization {
58                detail: e.to_string(),
59            })?;
60
61        self.storage
62            .batch_write(&[
63                WriteOp::Put {
64                    ns: Namespace::Meta,
65                    key: meta_key.into_bytes(),
66                    value: schema_bytes,
67                },
68                WriteOp::Put {
69                    ns: Namespace::Meta,
70                    key: META_COLUMNAR_COLLECTIONS.to_vec(),
71                    value: names_bytes,
72                },
73            ])
74            .await?;
75
76        let mutation = MutationEngine::new(name.to_string(), schema);
77        let state = CollectionState {
78            mutation,
79            profile,
80            bitemporal,
81            segments: Vec::new(),
82            next_segment_id: 1,
83        };
84
85        let mut guard = self
86            .collections
87            .write()
88            .map_err(|_| LiteError::LockPoisoned)?;
89        if guard.contains_key(name) {
90            return Err(LiteError::BadRequest {
91                detail: format!(
92                    "columnar collection '{name}' was created concurrently by another writer"
93                ),
94            });
95        }
96        guard.insert(name.to_string(), Arc::new(Mutex::new(state)));
97
98        Ok(())
99    }
100
101    /// Drop a columnar collection and all its data.
102    pub async fn drop_collection(&self, name: &str) -> Result<(), LiteError> {
103        // Snapshot state and remove from map under write lock.
104        let (segments, remaining_names): (Vec<SegmentMeta>, Vec<String>) = {
105            let mut guard = self
106                .collections
107                .write()
108                .map_err(|_| LiteError::LockPoisoned)?;
109            let state_arc = guard
110                .remove(name)
111                .ok_or_else(|| LiteError::collection_not_found("columnar", name))?;
112            let segments = {
113                let s = state_arc.lock().map_err(|_| LiteError::LockPoisoned)?;
114                s.segments.clone()
115            };
116            let names: Vec<String> = guard.keys().cloned().collect();
117            (segments, names)
118        };
119
120        // Delete large segment bytes via the segment ext (or KV fallback).
121        for seg in &segments {
122            if seg.fully_deleted_at_ms.is_none() {
123                remove_segment_bytes(&*self.storage, name, seg.segment_id).await?;
124            }
125        }
126
127        // Remove small B+ tree entries (delete bitmaps, metadata, schema).
128        let mut ops = Vec::new();
129        for seg in &segments {
130            ops.push(WriteOp::Delete {
131                ns: Namespace::Columnar,
132                key: format!("{name}:del:{}", seg.segment_id).into_bytes(),
133            });
134        }
135        ops.push(WriteOp::Delete {
136            ns: Namespace::Columnar,
137            key: format!("{name}:meta").into_bytes(),
138        });
139        ops.push(WriteOp::Delete {
140            ns: Namespace::Meta,
141            key: format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}").into_bytes(),
142        });
143
144        let names_bytes =
145            zerompk::to_msgpack_vec(&remaining_names).map_err(|e| LiteError::Serialization {
146                detail: e.to_string(),
147            })?;
148        ops.push(WriteOp::Put {
149            ns: Namespace::Meta,
150            key: META_COLUMNAR_COLLECTIONS.to_vec(),
151            value: names_bytes,
152        });
153
154        self.storage.batch_write(&ops).await?;
155        Ok(())
156    }
157
158    /// Add a column to an existing columnar collection.
159    pub async fn alter_add_column(
160        &self,
161        name: &str,
162        column: nodedb_types::columnar::ColumnDef,
163    ) -> Result<(), LiteError> {
164        if !column.nullable && column.default.is_none() {
165            return Err(LiteError::BadRequest {
166                detail: format!(
167                    "ALTER ADD COLUMN '{}': non-nullable column must have a DEFAULT",
168                    column.name
169                ),
170            });
171        }
172
173        let state_arc = self.lookup(name)?;
174
175        // Snapshot current schema + profile under inner lock.
176        let (mut schema, profile) = {
177            let s = Self::lock_state(&state_arc)?;
178            if s.mutation
179                .schema()
180                .columns
181                .iter()
182                .any(|c| c.name == column.name)
183            {
184                return Err(LiteError::BadRequest {
185                    detail: format!("column '{}' already exists in '{name}'", column.name),
186                });
187            }
188            (s.mutation.schema().clone(), s.profile.clone())
189        };
190
191        schema.columns.push(column);
192        schema.version = schema.version.saturating_add(1);
193
194        // Persist updated schema (lock dropped).
195        #[derive(serde::Serialize, zerompk::ToMessagePack)]
196        struct StoredSchema<'a> {
197            schema: &'a ColumnarSchema,
198            profile: &'a ColumnarProfile,
199        }
200        let meta_key = format!("{META_COLUMNAR_SCHEMA_PREFIX}{name}");
201        let schema_bytes = zerompk::to_msgpack_vec(&StoredSchema {
202            schema: &schema,
203            profile: &profile,
204        })
205        .map_err(|e| LiteError::Serialization {
206            detail: e.to_string(),
207        })?;
208
209        self.storage
210            .put(Namespace::Meta, meta_key.as_bytes(), &schema_bytes)
211            .await?;
212
213        // Swap in the new MutationEngine.
214        let mut s = Self::lock_state(&state_arc)?;
215        s.mutation = MutationEngine::new(name.to_string(), schema);
216
217        Ok(())
218    }
219
220    /// Get the schema for a collection (returns a clone).
221    pub fn schema(&self, name: &str) -> Option<ColumnarSchema> {
222        let guard = self.collections.read().ok()?;
223        let state_arc = guard.get(name)?;
224        let s = state_arc.lock().ok()?;
225        Some(s.mutation.schema().clone())
226    }
227
228    /// Get the profile for a collection (returns a clone).
229    pub fn profile(&self, name: &str) -> Option<ColumnarProfile> {
230        let guard = self.collections.read().ok()?;
231        let state_arc = guard.get(name)?;
232        let s = state_arc.lock().ok()?;
233        Some(s.profile.clone())
234    }
235
236    /// List all columnar collection names.
237    pub fn collection_names(&self) -> Vec<String> {
238        self.collections
239            .read()
240            .map(|g| g.keys().cloned().collect())
241            .unwrap_or_default()
242    }
243}