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(crate) fn from_raw_columns(
208 schema: &ColumnarSchema,
209 columns: Vec<ColumnData>,
210 row_count: usize,
211 ) -> Self {
212 Self {
213 schema: schema.clone(),
214 columns,
215 row_count,
216 flush_threshold: DEFAULT_FLUSH_THRESHOLD,
217 }
218 }
219
220 pub fn add_column(&mut self, name: String, column_type: ColumnType, nullable: bool) {
222 if self.schema.columns.iter().any(|c| c.name == name) {
223 return;
224 }
225
226 let existing_rows = self.row_count;
227 let mut col = ColumnData::new(&column_type, nullable);
228 if existing_rows > 0 {
229 col.backfill_nulls(existing_rows);
230 }
231
232 self.columns.push(col);
233 let col_def = if nullable {
234 ColumnDef::nullable(name, column_type)
235 } else {
236 ColumnDef::required(name, column_type)
237 };
238 self.schema.columns.push(col_def);
239 }
240}
241
242#[derive(Debug, Clone, Copy)]
244#[non_exhaustive]
245pub enum IngestValue<'a> {
246 Null,
247 Int64(i64),
248 Float64(f64),
249 Bool(bool),
250 Timestamp(i64),
251 Str(&'a str),
253}
254
255pub struct MemtableRowIter<'a> {
257 columns: &'a [ColumnData],
258 row_count: usize,
259 current: usize,
260}
261
262impl Iterator for MemtableRowIter<'_> {
263 type Item = Vec<Value>;
264
265 fn next(&mut self) -> Option<Self::Item> {
266 if self.current >= self.row_count {
267 return None;
268 }
269 let mut row = Vec::with_capacity(self.columns.len());
270 for col in self.columns {
271 row.push(col.get_value(self.current));
272 }
273 self.current += 1;
274 Some(row)
275 }
276
277 fn size_hint(&self) -> (usize, Option<usize>) {
278 let remaining = self.row_count - self.current;
279 (remaining, Some(remaining))
280 }
281}
282
283impl ExactSizeIterator for MemtableRowIter<'_> {}
284
285#[cfg(test)]
286mod tests {
287 use nodedb_types::columnar::{ColumnDef, ColumnType, ColumnarSchema};
288
289 use super::*;
290
291 fn test_schema() -> ColumnarSchema {
292 ColumnarSchema::new(vec![
293 ColumnDef::required("id", ColumnType::Int64).with_primary_key(),
294 ColumnDef::required("name", ColumnType::String),
295 ColumnDef::nullable("score", ColumnType::Float64),
296 ])
297 .expect("valid schema")
298 }
299
300 #[test]
301 fn append_and_count() {
302 let schema = test_schema();
303 let mut mt = ColumnarMemtable::new(&schema);
304
305 mt.append_row(&[
306 Value::Integer(1),
307 Value::String("Alice".into()),
308 Value::Float(0.75),
309 ])
310 .expect("append");
311
312 mt.append_row(&[Value::Integer(2), Value::String("Bob".into()), Value::Null])
313 .expect("append");
314
315 assert_eq!(mt.row_count(), 2);
316 assert!(!mt.is_empty());
317 }
318
319 #[test]
320 fn null_violation_rejected() {
321 let schema = test_schema();
322 let mut mt = ColumnarMemtable::new(&schema);
323
324 let err = mt
325 .append_row(&[Value::Null, Value::String("x".into()), Value::Null])
326 .unwrap_err();
327 assert!(matches!(err, ColumnarError::NullViolation(ref s) if s == "id"));
328 }
329
330 #[test]
331 fn schema_mismatch_rejected() {
332 let schema = test_schema();
333 let mut mt = ColumnarMemtable::new(&schema);
334
335 let err = mt.append_row(&[Value::Integer(1)]).unwrap_err();
336 assert!(matches!(err, ColumnarError::SchemaMismatch { .. }));
337 }
338
339 #[test]
340 fn flush_threshold() {
341 let schema = test_schema();
342 let mut mt = ColumnarMemtable::with_threshold(&schema, 3);
343
344 for i in 0..2 {
345 mt.append_row(&[
346 Value::Integer(i),
347 Value::String(format!("u{i}")),
348 Value::Null,
349 ])
350 .expect("append");
351 }
352 assert!(!mt.should_flush());
353
354 mt.append_row(&[Value::Integer(2), Value::String("u2".into()), Value::Null])
355 .expect("append");
356 assert!(mt.should_flush());
357 }
358
359 #[test]
360 fn drain_resets() {
361 let schema = test_schema();
362 let mut mt = ColumnarMemtable::new(&schema);
363
364 mt.append_row(&[
365 Value::Integer(1),
366 Value::String("x".into()),
367 Value::Float(0.5),
368 ])
369 .expect("append");
370
371 let (_schema, columns, row_count) = mt.drain();
372 assert_eq!(row_count, 1);
373 assert_eq!(columns.len(), 3);
374 assert_eq!(mt.row_count(), 0);
375 assert!(mt.is_empty());
376
377 match &columns[0] {
378 ColumnData::Int64 { values, valid } => {
379 assert_eq!(values, &[1]);
380 assert!(valid.is_none());
381 }
382 _ => panic!("expected Int64"),
383 }
384 match &columns[1] {
385 ColumnData::String {
386 data,
387 offsets,
388 valid,
389 } => {
390 assert_eq!(std::str::from_utf8(data).unwrap(), "x");
391 assert_eq!(offsets, &[0, 1]);
392 assert!(valid.is_none());
393 }
394 _ => panic!("expected String"),
395 }
396 }
397
398 #[test]
399 fn all_types() {
400 let schema = ColumnarSchema::new(vec![
401 ColumnDef::required("i", ColumnType::Int64),
402 ColumnDef::required("f", ColumnType::Float64),
403 ColumnDef::required("b", ColumnType::Bool),
404 ColumnDef::required("ts", ColumnType::Timestamp),
405 ColumnDef::required("s", ColumnType::String),
406 ColumnDef::required("raw", ColumnType::Bytes),
407 ColumnDef::required("vec", ColumnType::Vector(3)),
408 ])
409 .expect("valid");
410
411 let mut mt = ColumnarMemtable::new(&schema);
412 mt.append_row(&[
413 Value::Integer(42),
414 Value::Float(0.25),
415 Value::Bool(true),
416 Value::Integer(1_700_000_000),
417 Value::String("hello".into()),
418 Value::Bytes(vec![0xDE, 0xAD]),
419 Value::Array(vec![
420 Value::Float(1.0),
421 Value::Float(2.0),
422 Value::Float(3.0),
423 ]),
424 ])
425 .expect("append all types");
426
427 assert_eq!(mt.row_count(), 1);
428 }
429
430 #[test]
431 fn dict_encode_low_cardinality() {
432 let schema = ColumnarSchema::new(vec![ColumnDef::required("qtype", ColumnType::String)])
433 .expect("valid");
434
435 let mut mt = ColumnarMemtable::new(&schema);
436 let qtypes = ["A", "B", "AAAA", "NS", "MX", "SOA", "CNAME", "PTR"];
437 for _ in 0..10 {
438 for &q in &qtypes {
439 mt.append_row(&[Value::String(q.into())]).expect("append");
440 }
441 }
442 assert_eq!(mt.row_count(), 80);
443
444 mt.try_dict_encode_columns(DICT_ENCODE_MAX_CARDINALITY);
445
446 let (_schema, columns, _row_count) = mt.drain();
447 match &columns[0] {
448 ColumnData::DictEncoded {
449 ids,
450 dictionary,
451 valid,
452 ..
453 } => {
454 assert_eq!(ids.len(), 80);
455 assert!(valid.is_none());
456 assert_eq!(dictionary.len(), 8);
457 for &id in ids {
458 assert!((id as usize) < dictionary.len());
459 }
460 for (i, &q) in qtypes.iter().enumerate().take(8) {
461 let expected_id = dictionary.iter().position(|s| s == q).expect("in dict");
462 assert_eq!(ids[i], expected_id as u32);
463 }
464 }
465 _ => panic!("expected DictEncoded after try_dict_encode_columns"),
466 }
467 }
468
469 #[test]
470 fn dict_encode_exceeds_cardinality_stays_string() {
471 let schema = ColumnarSchema::new(vec![ColumnDef::required("name", ColumnType::String)])
472 .expect("valid");
473
474 let mut mt = ColumnarMemtable::new(&schema);
475 let max: u32 = 4;
476 for i in 0..=max {
477 mt.append_row(&[Value::String(format!("val_{i}"))])
478 .expect("append");
479 }
480
481 mt.try_dict_encode_columns(max);
482
483 let (_schema, columns, _row_count) = mt.drain();
484 assert!(matches!(columns[0], ColumnData::String { .. }));
485 }
486
487 #[test]
488 fn dict_encode_with_nulls() {
489 let schema = ColumnarSchema::new(vec![ColumnDef::nullable("tag", ColumnType::String)])
490 .expect("valid");
491
492 let mut mt = ColumnarMemtable::new(&schema);
493 mt.append_row(&[Value::String("foo".into())])
494 .expect("append");
495 mt.append_row(&[Value::Null]).expect("append null");
496 mt.append_row(&[Value::String("bar".into())])
497 .expect("append");
498 mt.append_row(&[Value::Null]).expect("append null");
499
500 mt.try_dict_encode_columns(DICT_ENCODE_MAX_CARDINALITY);
501
502 let (_schema, columns, _row_count) = mt.drain();
503 match &columns[0] {
504 ColumnData::DictEncoded {
505 ids,
506 valid,
507 dictionary,
508 ..
509 } => {
510 assert_eq!(ids.len(), 4);
511 let v = valid.as_ref().expect("nullable column has validity bitmap");
512 assert_eq!(v.len(), 4);
513 assert!(v[0]);
514 assert!(!v[1]);
515 assert!(v[2]);
516 assert!(!v[3]);
517 assert_eq!(dictionary.len(), 2);
518 }
519 _ => panic!("expected DictEncoded"),
520 }
521 }
522}