1mod column_data;
14
15pub use column_data::{ColumnData, DICT_ENCODE_MAX_CARDINALITY};
16
17use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
18use nodedb_types::value::Value;
19
20use crate::error::ColumnarError;
21
22pub const DEFAULT_FLUSH_THRESHOLD: usize = 65_536;
24
25pub struct ColumnarMemtable {
30 schema: ColumnarSchema,
31 columns: Vec<ColumnData>,
32 row_count: usize,
33 flush_threshold: usize,
34}
35
36impl ColumnarMemtable {
37 pub fn new(schema: &ColumnarSchema) -> Self {
39 Self::with_threshold(schema, DEFAULT_FLUSH_THRESHOLD)
40 }
41
42 pub fn with_threshold(schema: &ColumnarSchema, flush_threshold: usize) -> Self {
44 let columns = schema
45 .columns
46 .iter()
47 .map(|col| ColumnData::new(&col.column_type, col.nullable))
48 .collect();
49 Self {
50 schema: schema.clone(),
51 columns,
52 row_count: 0,
53 flush_threshold,
54 }
55 }
56
57 pub fn append_row(&mut self, values: &[Value]) -> Result<(), ColumnarError> {
59 if values.len() != self.schema.columns.len() {
60 return Err(ColumnarError::SchemaMismatch {
61 expected: self.schema.columns.len(),
62 got: values.len(),
63 });
64 }
65
66 for (i, (col_def, value)) in self.schema.columns.iter().zip(values.iter()).enumerate() {
67 if matches!(value, Value::Null) && !col_def.nullable {
68 return Err(ColumnarError::NullViolation(col_def.name.clone()));
69 }
70 self.columns[i].push(value, &col_def.name, &col_def.column_type)?;
71 }
72
73 self.row_count += 1;
74 debug_assert!(
75 self.columns.iter().all(|c| c.len() == self.row_count),
76 "column lengths must stay aligned with row_count"
77 );
78 Ok(())
79 }
80
81 pub fn row_count(&self) -> usize {
83 self.row_count
84 }
85
86 pub fn should_flush(&self) -> bool {
88 self.row_count >= self.flush_threshold
89 }
90
91 pub fn is_empty(&self) -> bool {
93 self.row_count == 0
94 }
95
96 pub fn schema(&self) -> &ColumnarSchema {
98 &self.schema
99 }
100
101 pub fn columns(&self) -> &[ColumnData] {
103 &self.columns
104 }
105
106 pub fn try_dict_encode_columns(&mut self, max_cardinality: u32) {
108 for col in &mut self.columns {
109 if let ColumnData::String { .. } = col
110 && let Some(encoded) = ColumnData::try_dict_encode(col, max_cardinality)
111 {
112 *col = encoded;
113 }
114 }
115 }
116
117 pub fn iter_rows(&self) -> MemtableRowIter<'_> {
119 MemtableRowIter {
120 columns: &self.columns,
121 row_count: self.row_count,
122 current: 0,
123 }
124 }
125
126 pub fn get_row(&self, row_idx: usize) -> Option<Vec<Value>> {
128 if row_idx >= self.row_count {
129 return None;
130 }
131 let mut row = Vec::with_capacity(self.columns.len());
132 for col in &self.columns {
133 row.push(col.get_value(row_idx));
134 }
135 Some(row)
136 }
137
138 pub fn truncate_to(&mut self, n: usize) {
143 if n >= self.row_count {
144 return;
145 }
146 for col in &mut self.columns {
147 col.truncate(n);
148 }
149 self.row_count = n;
150 debug_assert!(
151 self.columns.iter().all(|c| c.len() == self.row_count),
152 "column lengths misaligned after truncate_to"
153 );
154 }
155
156 pub fn drain(&mut self) -> (ColumnarSchema, Vec<ColumnData>, usize) {
158 let columns = std::mem::replace(
159 &mut self.columns,
160 self.schema
161 .columns
162 .iter()
163 .map(|col| ColumnData::new(&col.column_type, col.nullable))
164 .collect(),
165 );
166 let row_count = self.row_count;
167 self.row_count = 0;
168 (self.schema.clone(), columns, row_count)
169 }
170
171 pub fn drain_optimized(&mut self) -> (ColumnarSchema, Vec<ColumnData>, usize) {
173 self.try_dict_encode_columns(DICT_ENCODE_MAX_CARDINALITY);
174 self.drain()
175 }
176
177 pub fn ingest_row_refs(&mut self, values: &[IngestValue<'_>]) -> Result<(), ColumnarError> {
182 if values.len() != self.schema.columns.len() {
183 return Err(ColumnarError::SchemaMismatch {
184 expected: self.schema.columns.len(),
185 got: values.len(),
186 });
187 }
188
189 for (i, (col_def, value)) in self.schema.columns.iter().zip(values.iter()).enumerate() {
190 if matches!(value, IngestValue::Null) && !col_def.nullable {
191 return Err(ColumnarError::NullViolation(col_def.name.clone()));
192 }
193 self.columns[i].push_ref(value, &col_def.name)?;
194 }
195
196 self.row_count += 1;
197 Ok(())
198 }
199
200 pub fn add_column(&mut self, name: String, column_type: ColumnType, nullable: bool) {
202 if self.schema.columns.iter().any(|c| c.name == name) {
203 return;
204 }
205
206 let existing_rows = self.row_count;
207 let mut col = ColumnData::new(&column_type, nullable);
208 if existing_rows > 0 {
209 col.backfill_nulls(existing_rows);
210 }
211
212 self.columns.push(col);
213 let col_def = if nullable {
214 ColumnDef::nullable(name, column_type)
215 } else {
216 ColumnDef::required(name, column_type)
217 };
218 self.schema.columns.push(col_def);
219 }
220}
221
222#[derive(Debug, Clone, Copy)]
224#[non_exhaustive]
225pub enum IngestValue<'a> {
226 Null,
227 Int64(i64),
228 Float64(f64),
229 Bool(bool),
230 Timestamp(i64),
231 Str(&'a str),
233}
234
235pub struct MemtableRowIter<'a> {
237 columns: &'a [ColumnData],
238 row_count: usize,
239 current: usize,
240}
241
242impl Iterator for MemtableRowIter<'_> {
243 type Item = Vec<Value>;
244
245 fn next(&mut self) -> Option<Self::Item> {
246 if self.current >= self.row_count {
247 return None;
248 }
249 let mut row = Vec::with_capacity(self.columns.len());
250 for col in self.columns {
251 row.push(col.get_value(self.current));
252 }
253 self.current += 1;
254 Some(row)
255 }
256
257 fn size_hint(&self) -> (usize, Option<usize>) {
258 let remaining = self.row_count - self.current;
259 (remaining, Some(remaining))
260 }
261}
262
263impl ExactSizeIterator for MemtableRowIter<'_> {}
264
265#[cfg(test)]
266mod tests {
267 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
268
269 use super::*;
270
271 fn test_schema() -> ColumnarSchema {
272 ColumnarSchema::new(vec![
273 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
274 ColumnDef::required("name", ColumnType::String),
275 ColumnDef::nullable("score", ColumnType::Float64),
276 ])
277 .expect("valid schema")
278 }
279
280 #[test]
281 fn append_and_count() {
282 let schema = test_schema();
283 let mut mt = ColumnarMemtable::new(&schema);
284
285 mt.append_row(&[
286 Value::Integer(1),
287 Value::String("Alice".into()),
288 Value::Float(0.75),
289 ])
290 .expect("append");
291
292 mt.append_row(&[Value::Integer(2), Value::String("Bob".into()), Value::Null])
293 .expect("append");
294
295 assert_eq!(mt.row_count(), 2);
296 assert!(!mt.is_empty());
297 }
298
299 #[test]
300 fn null_violation_rejected() {
301 let schema = test_schema();
302 let mut mt = ColumnarMemtable::new(&schema);
303
304 let err = mt
305 .append_row(&[Value::Null, Value::String("x".into()), Value::Null])
306 .unwrap_err();
307 assert!(matches!(err, ColumnarError::NullViolation(ref s) if s == "id"));
308 }
309
310 #[test]
311 fn schema_mismatch_rejected() {
312 let schema = test_schema();
313 let mut mt = ColumnarMemtable::new(&schema);
314
315 let err = mt.append_row(&[Value::Integer(1)]).unwrap_err();
316 assert!(matches!(err, ColumnarError::SchemaMismatch { .. }));
317 }
318
319 #[test]
320 fn flush_threshold() {
321 let schema = test_schema();
322 let mut mt = ColumnarMemtable::with_threshold(&schema, 3);
323
324 for i in 0..2 {
325 mt.append_row(&[
326 Value::Integer(i),
327 Value::String(format!("u{i}")),
328 Value::Null,
329 ])
330 .expect("append");
331 }
332 assert!(!mt.should_flush());
333
334 mt.append_row(&[Value::Integer(2), Value::String("u2".into()), Value::Null])
335 .expect("append");
336 assert!(mt.should_flush());
337 }
338
339 #[test]
340 fn drain_resets() {
341 let schema = test_schema();
342 let mut mt = ColumnarMemtable::new(&schema);
343
344 mt.append_row(&[
345 Value::Integer(1),
346 Value::String("x".into()),
347 Value::Float(0.5),
348 ])
349 .expect("append");
350
351 let (_schema, columns, row_count) = mt.drain();
352 assert_eq!(row_count, 1);
353 assert_eq!(columns.len(), 3);
354 assert_eq!(mt.row_count(), 0);
355 assert!(mt.is_empty());
356
357 match &columns[0] {
358 ColumnData::Int64 { values, valid } => {
359 assert_eq!(values, &[1]);
360 assert!(valid.is_none());
361 }
362 _ => panic!("expected Int64"),
363 }
364 match &columns[1] {
365 ColumnData::String {
366 data,
367 offsets,
368 valid,
369 } => {
370 assert_eq!(std::str::from_utf8(data).unwrap(), "x");
371 assert_eq!(offsets, &[0, 1]);
372 assert!(valid.is_none());
373 }
374 _ => panic!("expected String"),
375 }
376 }
377
378 #[test]
379 fn all_types() {
380 let schema = ColumnarSchema::new(vec![
381 ColumnDef::required("i", ColumnType::Int64),
382 ColumnDef::required("f", ColumnType::Float64),
383 ColumnDef::required("b", ColumnType::Bool),
384 ColumnDef::required("ts", ColumnType::Timestamp),
385 ColumnDef::required("s", ColumnType::String),
386 ColumnDef::required("raw", ColumnType::Bytes),
387 ColumnDef::required("vec", ColumnType::Vector(3)),
388 ])
389 .expect("valid");
390
391 let mut mt = ColumnarMemtable::new(&schema);
392 mt.append_row(&[
393 Value::Integer(42),
394 Value::Float(0.25),
395 Value::Bool(true),
396 Value::Integer(1_700_000_000),
397 Value::String("hello".into()),
398 Value::Bytes(vec![0xDE, 0xAD]),
399 Value::Array(vec![
400 Value::Float(1.0),
401 Value::Float(2.0),
402 Value::Float(3.0),
403 ]),
404 ])
405 .expect("append all types");
406
407 assert_eq!(mt.row_count(), 1);
408 }
409
410 #[test]
411 fn dict_encode_low_cardinality() {
412 let schema = ColumnarSchema::new(vec![ColumnDef::required("qtype", ColumnType::String)])
413 .expect("valid");
414
415 let mut mt = ColumnarMemtable::new(&schema);
416 let qtypes = ["A", "B", "AAAA", "NS", "MX", "SOA", "CNAME", "PTR"];
417 for _ in 0..10 {
418 for &q in &qtypes {
419 mt.append_row(&[Value::String(q.into())]).expect("append");
420 }
421 }
422 assert_eq!(mt.row_count(), 80);
423
424 mt.try_dict_encode_columns(DICT_ENCODE_MAX_CARDINALITY);
425
426 let (_schema, columns, _row_count) = mt.drain();
427 match &columns[0] {
428 ColumnData::DictEncoded {
429 ids,
430 dictionary,
431 valid,
432 ..
433 } => {
434 assert_eq!(ids.len(), 80);
435 assert!(valid.is_none());
436 assert_eq!(dictionary.len(), 8);
437 for &id in ids {
438 assert!((id as usize) < dictionary.len());
439 }
440 for (i, &q) in qtypes.iter().enumerate().take(8) {
441 let expected_id = dictionary.iter().position(|s| s == q).expect("in dict");
442 assert_eq!(ids[i], expected_id as u32);
443 }
444 }
445 _ => panic!("expected DictEncoded after try_dict_encode_columns"),
446 }
447 }
448
449 #[test]
450 fn dict_encode_exceeds_cardinality_stays_string() {
451 let schema = ColumnarSchema::new(vec![ColumnDef::required("name", ColumnType::String)])
452 .expect("valid");
453
454 let mut mt = ColumnarMemtable::new(&schema);
455 let max: u32 = 4;
456 for i in 0..=max {
457 mt.append_row(&[Value::String(format!("val_{i}"))])
458 .expect("append");
459 }
460
461 mt.try_dict_encode_columns(max);
462
463 let (_schema, columns, _row_count) = mt.drain();
464 assert!(matches!(columns[0], ColumnData::String { .. }));
465 }
466
467 #[test]
468 fn dict_encode_with_nulls() {
469 let schema = ColumnarSchema::new(vec![ColumnDef::nullable("tag", ColumnType::String)])
470 .expect("valid");
471
472 let mut mt = ColumnarMemtable::new(&schema);
473 mt.append_row(&[Value::String("foo".into())])
474 .expect("append");
475 mt.append_row(&[Value::Null]).expect("append null");
476 mt.append_row(&[Value::String("bar".into())])
477 .expect("append");
478 mt.append_row(&[Value::Null]).expect("append null");
479
480 mt.try_dict_encode_columns(DICT_ENCODE_MAX_CARDINALITY);
481
482 let (_schema, columns, _row_count) = mt.drain();
483 match &columns[0] {
484 ColumnData::DictEncoded {
485 ids,
486 valid,
487 dictionary,
488 ..
489 } => {
490 assert_eq!(ids.len(), 4);
491 let v = valid.as_ref().expect("nullable column has validity bitmap");
492 assert_eq!(v.len(), 4);
493 assert!(v[0]);
494 assert!(!v[1]);
495 assert!(v[2]);
496 assert!(!v[3]);
497 assert_eq!(dictionary.len(), 2);
498 }
499 _ => panic!("expected DictEncoded"),
500 }
501 }
502}