velesdb_core/column_store/
mod.rs1#![allow(clippy::cast_precision_loss)]
33#![allow(clippy::cast_possible_truncation)]
34#![allow(clippy::doc_markdown)] mod batch;
37#[cfg(test)]
38mod batch_tests;
39mod filter;
40mod filter_array;
41mod filter_geo;
42#[cfg(test)]
43mod filter_tests;
44pub(crate) mod haversine;
45#[cfg(test)]
46mod haversine_tests;
47mod primary_key_ops;
48mod string_table;
49mod types;
50mod vacuum;
51#[cfg(test)]
52mod vacuum_tests;
53
54use roaring::RoaringBitmap;
55use rustc_hash::FxHashMap;
56use std::collections::HashMap;
57
58pub use filter_geo::{CompareOp, GeoBboxParams, GeoDistanceParams};
59pub use string_table::StringTable;
60pub use types::{
61 AutoVacuumConfig, BatchUpdate, BatchUpdateResult, BatchUpsertResult, ColumnStoreError,
62 ColumnType, ColumnValue, ExpireResult, StringId, TypedColumn, UpsertResult, VacuumConfig,
63 VacuumStats,
64};
65
66#[derive(Debug, Default)]
68pub struct ColumnStore {
69 pub(crate) columns: HashMap<String, TypedColumn>,
71 pub(crate) string_table: StringTable,
73 pub(crate) row_count: usize,
75 pub(crate) primary_key_column: Option<String>,
77 pub(crate) primary_index: HashMap<i64, usize>,
79 pub(crate) row_idx_to_pk: HashMap<usize, i64>,
81 pub(crate) deletion_bitmap: RoaringBitmap,
85 pub(crate) row_expiry: HashMap<usize, u64>,
87}
88
89impl ColumnStore {
90 #[must_use]
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 #[must_use]
98 pub fn with_schema(fields: &[(&str, ColumnType)]) -> Self {
99 let mut store = Self::new();
100 for (name, col_type) in fields {
101 store.add_column(name, col_type);
102 }
103 store
104 }
105
106 pub fn with_schema_validated(fields: &[(&str, ColumnType)]) -> Result<Self, ColumnStoreError> {
112 for (name, col_type) in fields {
113 Self::reject_nested_array(name, col_type)?;
114 }
115 Ok(Self::with_schema(fields))
116 }
117
118 pub fn with_primary_key(
125 fields: &[(&str, ColumnType)],
126 pk_column: &str,
127 ) -> crate::error::Result<Self> {
128 let pk_field = fields
129 .iter()
130 .find(|(name, _)| *name == pk_column)
131 .ok_or_else(|| {
132 crate::error::Error::ColumnStoreError(format!(
133 "Primary key column '{}' not found in fields: {:?}",
134 pk_column,
135 fields.iter().map(|(n, _)| *n).collect::<Vec<_>>()
136 ))
137 })?;
138 if !matches!(pk_field.1, ColumnType::Int) {
139 return Err(crate::error::Error::ColumnStoreError(format!(
140 "Primary key column '{}' must be Int type, got {:?}",
141 pk_column, pk_field.1
142 )));
143 }
144
145 let mut store = Self::with_schema(fields);
146 store.primary_key_column = Some(pk_column.to_string());
147 store.primary_index = HashMap::new();
148 Ok(store)
149 }
150
151 #[must_use]
153 pub fn primary_key_column(&self) -> Option<&str> {
154 self.primary_key_column.as_deref()
155 }
156
157 pub fn add_column(&mut self, name: &str, col_type: &ColumnType) {
175 debug_assert!(
176 self.row_count == 0,
177 "ColumnStore::add_column called with {} existing row(s): the new column '{name}' \
178 would not be backfilled and would desynchronize the store. \
179 Use add_column_backfilled instead.",
180 self.row_count
181 );
182 self.insert_empty_column(name, col_type);
183 }
184
185 pub(crate) fn add_column_backfilled(&mut self, name: &str, col_type: &ColumnType) {
189 self.insert_empty_column(name, col_type);
190 if let Some(column) = self.columns.get_mut(name) {
191 for _ in 0..self.row_count {
192 column.push_null();
193 }
194 }
195 }
196
197 fn insert_empty_column(&mut self, name: &str, col_type: &ColumnType) {
201 let column = match col_type {
202 ColumnType::Int => TypedColumn::new_int(0),
203 ColumnType::Float => TypedColumn::new_float(0),
204 ColumnType::String => TypedColumn::new_string(0),
205 ColumnType::Bool => TypedColumn::new_bool(0),
206 ColumnType::Array(inner) => TypedColumn::new_array((**inner).clone(), 0),
207 ColumnType::GeoPoint => TypedColumn::new_geopoint(0),
208 };
209 self.columns.insert(name.to_string(), column);
210 }
211
212 pub(crate) fn tombstone_row(&mut self, row_idx: usize) {
217 if row_idx >= self.row_count {
218 return;
219 }
220 if let Ok(idx) = u32::try_from(row_idx) {
221 self.deletion_bitmap.insert(idx);
222 }
223 }
224
225 #[must_use]
227 pub fn row_count(&self) -> usize {
228 self.row_count
229 }
230
231 #[must_use]
233 pub fn active_row_count(&self) -> usize {
234 self.row_count
235 .saturating_sub(self.deletion_bitmap.len() as usize)
236 }
237
238 #[must_use]
240 pub fn deleted_row_count(&self) -> usize {
241 self.deletion_bitmap.len() as usize
242 }
243
244 #[must_use]
246 pub fn string_table(&self) -> &StringTable {
247 &self.string_table
248 }
249
250 pub fn string_table_mut(&mut self) -> &mut StringTable {
252 &mut self.string_table
253 }
254
255 pub fn push_row_unchecked(&mut self, values: &[(&str, ColumnValue)]) {
257 let value_map: FxHashMap<&str, &ColumnValue> =
258 values.iter().map(|(k, v)| (*k, v)).collect();
259
260 for (name, column) in &mut self.columns {
261 if let Some(value) = value_map.get(name.as_str()) {
262 column.push_typed(value);
263 } else {
264 column.push_null();
265 }
266 }
267 self.row_count += 1;
268 }
269
270 #[inline]
272 pub fn push_row(&mut self, values: &[(&str, ColumnValue)]) {
273 self.push_row_unchecked(values);
274 }
275
276 #[must_use]
278 pub fn get_column(&self, name: &str) -> Option<&TypedColumn> {
279 self.columns.get(name)
280 }
281
282 fn reject_nested_array(name: &str, col_type: &ColumnType) -> Result<(), ColumnStoreError> {
284 if let ColumnType::Array(inner) = col_type {
285 if matches!(inner.as_ref(), ColumnType::Array(_)) {
286 return Err(ColumnStoreError::TypeMismatch {
287 expected: "scalar element type (Int, Float, String, Bool)".to_string(),
288 actual: format!("nested Array in column '{name}'"),
289 });
290 }
291 }
292 Ok(())
293 }
294
295 pub fn column_names(&self) -> impl Iterator<Item = &str> {
297 self.columns.keys().map(String::as_str)
298 }
299
300 #[must_use]
302 pub fn get_value_as_json(&self, column: &str, row_idx: usize) -> Option<serde_json::Value> {
303 if self.is_row_deleted_bitmap(row_idx) {
304 return None;
305 }
306
307 let col = self.columns.get(column)?;
308 if let TypedColumn::String(v) = col {
310 return v.get(row_idx).and_then(|opt| {
311 opt.and_then(|id| self.string_table.get(id).map(|s| serde_json::json!(s)))
312 });
313 }
314 if let TypedColumn::Array { data, .. } = col {
316 return self.get_array_as_json(data, row_idx);
317 }
318 if let TypedColumn::GeoPoint(v) = col {
320 return v
321 .get(row_idx)
322 .and_then(|opt| opt.map(|(lat, lng)| serde_json::json!({"lat": lat, "lng": lng})));
323 }
324 col.get_as_json_non_string(row_idx)
325 }
326
327 fn get_array_as_json(
329 &self,
330 data: &[Option<smallvec::SmallVec<[ColumnValue; 8]>>],
331 row_idx: usize,
332 ) -> Option<serde_json::Value> {
333 let arr = data.get(row_idx)?.as_ref()?;
334 let json_arr: Vec<serde_json::Value> =
335 arr.iter().map(|v| self.column_value_to_json(v)).collect();
336 Some(serde_json::Value::Array(json_arr))
337 }
338
339 fn column_value_to_json(&self, value: &ColumnValue) -> serde_json::Value {
341 match value {
342 ColumnValue::Int(v) => serde_json::json!(v),
343 ColumnValue::Float(v) => serde_json::json!(v),
344 ColumnValue::Bool(v) => serde_json::json!(v),
345 ColumnValue::String(id) => self
346 .string_table
347 .get(*id)
348 .map_or(serde_json::Value::Null, |s| serde_json::json!(s)),
349 ColumnValue::Null => serde_json::Value::Null,
350 ColumnValue::Array(inner) => {
351 let arr: Vec<serde_json::Value> =
352 inner.iter().map(|v| self.column_value_to_json(v)).collect();
353 serde_json::Value::Array(arr)
354 }
355 ColumnValue::GeoPoint(lat, lng) => {
356 serde_json::json!({"lat": lat, "lng": lng})
357 }
358 }
359 }
360}