1use crate::art_index::ArtPrimaryKeyIndex;
4use crate::art_key::ArtKey;
5use crate::column::Column;
6use crate::column_chunk::ColumnChunk;
7use crate::csr::CsrIndex;
8use crate::index::HashIndex;
9use crate::node_group::NodeGroup;
10use crate::spiller::Spiller;
11use crate::vector_index::VectorIndexTable;
12use akar_common::error::StorageError;
13use akar_common::types::{LogicalTypeID, Value, pk_value_to_string};
14use akar_vector::hnsw::DistanceMetric;
15use dashmap::DashMap;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19#[derive(Debug, Clone)]
21pub struct ColumnDefinition {
22 pub name: String,
23 pub logical_type: LogicalTypeID,
24 pub is_primary_key: bool,
25 pub compression: akar_common::enums::CompressionType,
26}
27
28#[derive(Debug, Clone)]
39pub struct NodeTable {
40 pub table_id: u64,
41 pub name: String,
42 pub columns: Vec<ColumnDefinition>,
43 pub primary_key_column: usize,
44 pub num_rows: u64,
45 pub node_groups: Vec<NodeGroup>,
48 pub hash_index: HashIndex<String>,
51 pub art_index: Option<ArtPrimaryKeyIndex>,
54 pub persistence_dirty: bool,
57 spiller: Option<Arc<Spiller>>,
62}
63
64pub const NO_PRIMARY_KEY: usize = usize::MAX;
70
71impl NodeTable {
72 pub fn new(table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> Self {
73 let primary_key_column = columns.iter().position(|c| c.is_primary_key).unwrap_or(NO_PRIMARY_KEY);
78 Self {
79 table_id,
80 name,
81 columns,
82 primary_key_column,
83 num_rows: 0,
84 node_groups: Vec::new(),
85 hash_index: HashIndex::new(),
86 art_index: None,
87 persistence_dirty: false,
88 spiller: None,
89 }
90 }
91
92 pub fn set_spiller(&mut self, spiller: Option<Arc<Spiller>>) {
95 self.spiller = spiller;
96 }
97
98 pub fn add_column(&mut self, column: ColumnDefinition) {
104 if self.columns.iter().any(|c| c.name.eq_ignore_ascii_case(&column.name)) {
105 return;
106 }
107 self.columns.push(column);
108 for group in &mut self.node_groups {
109 let mut chunk = ColumnChunk::new();
110 for _ in 0..group.num_nodes {
111 chunk.append(Value::Null);
112 }
113 group.columns.push(chunk);
114 }
115 self.persistence_dirty = true;
116 }
117
118 pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
133 self.insert_row_with_txn(values, None)
134 }
135
136 pub fn insert_row_with_txn(&mut self, mut values: Vec<Value>, txn_id: Option<u64>) -> Result<u64, StorageError> {
138 if values.len() != self.columns.len() {
139 return Err(StorageError::Page(format!(
140 "Column count mismatch: expected {} values, got {}",
141 self.columns.len(),
142 values.len()
143 )));
144 }
145
146 if self.primary_key_column < self.columns.len() {
148 let pk_value = &values[self.primary_key_column];
149 if matches!(pk_value, Value::Null) {
150 return Err(StorageError::Page(format!(
151 "NULL value not allowed for primary key column '{}' in table '{}'",
152 self.columns[self.primary_key_column].name, self.name
153 )));
154 }
155 }
156
157 coerce_values_to_columns(&mut values, &self.columns)?;
162
163 if self.primary_key_column < self.columns.len() {
165 let pk_value = &values[self.primary_key_column];
166 let pk_key = pk_value_to_string(pk_value);
167 if self.hash_index.lookup(&pk_key).is_some() {
168 return Err(StorageError::Index(format!(
169 "Duplicate primary key value: '{pk_key}' in table '{}'",
170 self.name
171 )));
172 }
173 }
174
175 let num_cols = self.columns.len();
177 if self.node_groups.is_empty() || self.node_groups.last().unwrap().is_full() {
178 let start_offset = self.num_rows;
179 let mut new_group = NodeGroup::new(num_cols, start_offset);
180 if txn_id.is_some() {
182 new_group.enable_version_info();
183 }
184 if let Some(ref spiller) = self.spiller {
185 new_group.set_spiller(spiller.clone());
186 }
187 self.node_groups.push(new_group);
188 }
189
190 let current = self.node_groups.last_mut().unwrap();
191 if txn_id.is_some() {
193 current.enable_version_info();
194 }
195 current.append_row_with_txn(values.clone(), txn_id)?;
196 current.restore_spilled()?;
199 self.num_rows += 1;
200
201 if self.primary_key_column < self.columns.len() {
203 let pk_value = &values[self.primary_key_column];
204 let pk_key = pk_value_to_string(pk_value);
205 self.hash_index.insert(pk_key, self.num_rows - 1);
206
207 if let Some(ref mut art_idx) = self.art_index
209 && let Some(art_key) = ArtKey::from_value(pk_value)
210 {
211 art_idx.insert(&art_key, self.num_rows - 1);
212 }
213 }
214
215 Ok(self.num_rows - 1)
216 }
217
218 pub fn insert_rows_batch(&mut self, rows: &[Vec<Value>]) -> Result<u64, StorageError> {
222 self.insert_rows_batch_with_txn(rows, None)
223 }
224
225 pub fn insert_rows_batch_with_txn(
227 &mut self,
228 rows: &[Vec<Value>],
229 txn_id: Option<u64>,
230 ) -> Result<u64, StorageError> {
231 if rows.is_empty() {
232 return Ok(0);
233 }
234 let num_cols = self.columns.len();
235
236 for (i, row) in rows.iter().enumerate() {
238 if row.len() != num_cols {
239 return Err(StorageError::Page(format!(
240 "Row {} column count mismatch: expected {} values, got {}",
241 i,
242 num_cols,
243 row.len()
244 )));
245 }
246 if self.primary_key_column < num_cols {
248 let pk_value = &row[self.primary_key_column];
249 if matches!(pk_value, Value::Null) {
250 return Err(StorageError::Page(format!(
251 "NULL value not allowed for primary key column '{}' in table '{}'",
252 self.columns[self.primary_key_column].name, self.name
253 )));
254 }
255 }
256 if self.primary_key_column < num_cols {
258 let pk_key = pk_value_to_string(&row[self.primary_key_column]);
259 if self.hash_index.lookup(&pk_key).is_some() {
260 return Err(StorageError::Index(format!(
261 "Duplicate primary key value: '{pk_key}' in table '{}'",
262 self.name
263 )));
264 }
265 }
266 }
267
268 let start_offset = self.num_rows;
269 let total_new = rows.len();
270
271 let mut coerced_rows = rows.to_vec();
275 for row in &mut coerced_rows {
276 coerce_values_to_columns(row, &self.columns)?;
277 }
278 let rows: &[Vec<Value>] = &coerced_rows;
279
280 if self.node_groups.is_empty() || self.node_groups.last().unwrap().is_full() {
282 let off = if self.node_groups.is_empty() {
283 start_offset
284 } else {
285 self.num_rows
286 };
287 let mut new_group = NodeGroup::new(num_cols, off);
288 if txn_id.is_some() {
289 new_group.enable_version_info();
290 }
291 if let Some(ref spiller) = self.spiller {
292 new_group.set_spiller(spiller.clone());
293 }
294 self.node_groups.push(new_group);
295 }
296
297 let mut inserted = 0usize;
299 while inserted < total_new {
300 let current = self.node_groups.last_mut().unwrap();
301 if txn_id.is_some() {
302 current.enable_version_info();
303 }
304 let rem = current.remaining();
305 let take = (total_new - inserted).min(rem);
306 for row in &rows[inserted..inserted + take] {
307 current.append_row_with_txn(row.clone(), txn_id)?;
308 }
309 self.num_rows += take as u64;
310 inserted += take;
311 if inserted < total_new {
312 let off = self.num_rows;
313 let mut new_group = NodeGroup::new(num_cols, off);
314 if txn_id.is_some() {
315 new_group.enable_version_info();
316 }
317 if let Some(ref spiller) = self.spiller {
318 new_group.set_spiller(spiller.clone());
319 }
320 self.node_groups.push(new_group);
321 }
322 }
323
324 for group in &mut self.node_groups {
327 group.restore_spilled()?;
328 }
329
330 for (i, row) in rows.iter().enumerate() {
332 if self.primary_key_column < num_cols {
333 let pk_key = pk_value_to_string(&row[self.primary_key_column]);
334 self.hash_index.insert(pk_key, start_offset + i as u64);
335 if let Some(ref mut art_idx) = self.art_index
336 && let Some(art_key) = ArtKey::from_value(&row[self.primary_key_column])
337 {
338 art_idx.insert(&art_key, start_offset + i as u64);
339 }
340 }
341 }
342
343 Ok(rows.len() as u64)
344 }
345
346 pub fn lookup_by_pk(&self, pk_value: &Value) -> Option<u64> {
351 let pk_key = pk_value_to_string(pk_value);
352 self.hash_index.lookup(&pk_key)
353 }
354
355 pub fn lookup_by_pk_batch(&self, pk_values: &[Value]) -> Vec<Option<u64>> {
362 pk_values
363 .iter()
364 .map(|pk_value| {
365 let pk_key = pk_value_to_string(pk_value);
366 self.hash_index.lookup(&pk_key)
367 })
368 .collect()
369 }
370
371 pub fn lookup_by_pk_range(
379 &self,
380 lower: Option<&Value>,
381 lower_inclusive: bool,
382 upper: Option<&Value>,
383 upper_inclusive: bool,
384 max_results: u64,
385 ) -> Vec<u64> {
386 match &self.art_index {
387 Some(idx) => {
388 let lower_key = lower.and_then(ArtKey::from_value);
389 let upper_key = upper.and_then(ArtKey::from_value);
390 idx.range_scan(
391 lower_key.as_ref(),
392 lower_inclusive,
393 upper_key.as_ref(),
394 upper_inclusive,
395 max_results,
396 )
397 }
398 None => Vec::new(),
399 }
400 }
401
402 pub fn scan_column(
411 &self,
412 col_idx: usize,
413 start: u64,
414 count: u64,
415 snapshot_ts: Option<u64>,
416 commit_history: &[(u64, u64)],
417 ) -> Vec<Value> {
418 if col_idx >= self.columns.len() || start >= self.num_rows {
419 return Vec::new();
420 }
421 let end = (start + count).min(self.num_rows);
422 let mut result = Vec::with_capacity((end - start) as usize);
423
424 let group_start = self.find_group(start);
426 let mut remaining = end - start;
427
428 for g_idx in group_start..self.node_groups.len() {
429 if remaining == 0 {
430 break;
431 }
432 let group = &self.node_groups[g_idx];
433 let local_start = if g_idx == group_start {
434 (start - group.start_offset) as usize
435 } else {
436 0
437 };
438 let available = (group.num_nodes as usize).saturating_sub(local_start);
439 let take = available.min(remaining as usize);
440
441 for row in local_start..local_start + take {
442 let val = group.get_value_with_snapshot(row, col_idx, snapshot_ts, commit_history);
443 match val {
444 Some(v) => result.push(v.clone()),
445 None => result.push(Value::Null),
446 }
447 }
448 remaining -= take as u64;
449 }
450
451 result
452 }
453
454 pub fn load_persisted_rows(&mut self, rows: Vec<Vec<Value>>) -> Result<(), StorageError> {
462 let num_cols = self.columns.len();
463 self.node_groups.clear();
464 self.hash_index.clear();
465
466 let mut offset = 0u64;
467 let mut group = NodeGroup::new(num_cols, offset);
468 for row in &rows {
469 if row.len() != num_cols {
470 return Err(StorageError::Page(format!(
471 "load_persisted_rows: expected {num_cols} values, got {}",
472 row.len()
473 )));
474 }
475 group.append_row_with_txn(row.clone(), None)?;
476 offset += 1;
477 if group.is_full() {
478 self.node_groups.push(group);
479 group = NodeGroup::new(num_cols, offset);
480 }
481 }
482 if group.num_nodes > 0 {
483 self.node_groups.push(group);
484 }
485 self.num_rows = offset;
486
487 if self.primary_key_column < num_cols {
489 for (row_idx, values) in rows.iter().enumerate() {
490 let pk = &values[self.primary_key_column];
491 if matches!(pk, Value::Null) {
492 continue;
493 }
494 let key = pk_value_to_string(pk);
495 self.hash_index.insert(key, row_idx as u64);
496 }
497 }
498
499 if self.art_index.is_some() && self.primary_key_column < num_cols {
501 if let Some(art) = &mut self.art_index {
502 art.clear();
503 for (row_idx, values) in rows.iter().enumerate() {
504 let pk = &values[self.primary_key_column];
505 if matches!(pk, Value::Null) {
506 continue;
507 }
508 if let Some(key) = ArtKey::from_value(pk) {
509 art.insert(&key, row_idx as u64);
510 }
511 }
512 }
513 }
514 Ok(())
515 }
516
517 pub fn update_cell(&mut self, row_idx: u64, col_idx: usize, value: Value) -> Result<(), StorageError> {
519 if col_idx >= self.columns.len() {
520 return Err(StorageError::Page(format!("Column index {col_idx} out of range")));
521 }
522 if row_idx >= self.num_rows {
523 return Err(StorageError::Page(format!(
524 "Row index {row_idx} out of range (num_rows={})",
525 self.num_rows
526 )));
527 }
528 self.persistence_dirty = true;
529
530 let mut offset = 0u64;
531 for group in &mut self.node_groups {
532 if row_idx < offset + group.num_nodes {
533 let local_row = (row_idx - offset) as usize;
534 if let Some(col_chunk) = group.columns.get_mut(col_idx) {
535 col_chunk.set_value(local_row, value)?;
536 }
537 return Ok(());
538 }
539 offset += group.num_nodes;
540 }
541 Err(StorageError::Page(format!(
542 "Row index {row_idx} not found in any node group"
543 )))
544 }
545
546 pub fn delete_row(&mut self, row_idx: u64) -> Result<(), StorageError> {
549 self.delete_row_with_txn(row_idx, None)
550 }
551
552 pub fn delete_row_with_txn(&mut self, row_idx: u64, txn_id: Option<u64>) -> Result<(), StorageError> {
557 if row_idx >= self.num_rows {
558 return Err(StorageError::Page(format!(
559 "Row index {row_idx} out of range (num_rows={})",
560 self.num_rows
561 )));
562 }
563 self.persistence_dirty = true;
564
565 let mut offset = 0u64;
567 for group in &mut self.node_groups {
568 if row_idx < offset + group.num_nodes {
569 let local_row = (row_idx - offset) as usize;
570 if let Some(txn) = txn_id {
572 if let Some(ref vi) = group.version_info {
573 vi.delete(txn, local_row as u32);
574 }
575 }
576 let pk_value = (self.primary_key_column < self.columns.len())
579 .then(|| group.columns.get(self.primary_key_column))
580 .flatten()
581 .and_then(|chunk| chunk.get(local_row))
582 .cloned();
583 for col_chunk in &mut group.columns {
585 let _ = col_chunk.set_value(local_row, Value::Null);
586 }
587 if let Some(pk) = pk_value
591 && !matches!(pk, Value::Null)
592 {
593 let pk_key = pk_value_to_string(&pk);
594 self.hash_index.delete(&pk_key);
595 if let Some(ref mut art_idx) = self.art_index
596 && let Some(art_key) = ArtKey::from_value(&pk)
597 {
598 art_idx.delete(&art_key, row_idx);
599 }
600 }
601 return Ok(());
602 }
603 offset += group.num_nodes;
604 }
605 Err(StorageError::Page(format!(
606 "Row index {row_idx} not found in any node group"
607 )))
608 }
609
610 pub fn row_undo_bytes(&self, row_idx: u64) -> Vec<u8> {
614 let mut out = Vec::new();
615 for col in 0..self.columns.len() {
616 let val = self.get_value(row_idx as usize, col).cloned().unwrap_or(Value::Null);
617 out.extend_from_slice(&Column::serialize_value(&val));
618 }
619 out
620 }
621
622 pub fn cell_undo_bytes(&self, row_idx: u64, col_idx: usize) -> Vec<u8> {
625 let val = self
626 .get_value(row_idx as usize, col_idx)
627 .cloned()
628 .unwrap_or(Value::Null);
629 Column::serialize_value(&val)
630 }
631
632 pub fn get_value(&self, row: usize, col: usize) -> Option<&Value> {
635 self.get_value_with_snapshot(row, col, None, &[])
636 }
637
638 pub fn get_value_with_snapshot(
643 &self,
644 row: usize,
645 col: usize,
646 snapshot_ts: Option<u64>,
647 commit_history: &[(u64, u64)],
648 ) -> Option<&Value> {
649 if col >= self.columns.len() || row as u64 >= self.num_rows {
650 return None;
651 }
652 let group_idx = self.find_group(row as u64);
653 let group = self.node_groups.get(group_idx)?;
654 let local_row = row as u64 - group.start_offset;
655 group.get_value_with_snapshot(local_row as usize, col, snapshot_ts, commit_history)
656 }
657
658 pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
662 self.to_column_major_data_with_predicate(None)
663 }
664
665 pub fn to_column_major_data_with_predicate(&self, predicate: Option<(usize, &str, &Value)>) -> Vec<Vec<Value>> {
668 let num_cols = self.columns.len();
669 let mut result = vec![Vec::new(); num_cols]; for group in &self.node_groups {
672 if let Some((col_idx, op, val)) = predicate
673 && let Some(col_chunk) = group.columns.get(col_idx)
674 {
675 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
676 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
677 continue; }
679 }
680
681 for row in 0..group.num_nodes as usize {
682 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
683 match group.get_value(row, col) {
684 Some(v) => res_col.push(v.clone()),
685 None => res_col.push(Value::Null),
686 }
687 }
688 }
689 }
690
691 result
692 }
693
694 pub fn to_column_major_data_with_snapshot(
700 &self,
701 snapshot_ts: Option<u64>,
702 commit_history: &[(u64, u64)],
703 ) -> Vec<Vec<Value>> {
704 let num_cols = self.columns.len();
705 let mut result = vec![Vec::new(); num_cols];
706
707 for group in &self.node_groups {
708 for row in 0..group.num_nodes as usize {
709 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
710 match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
711 Some(v) => res_col.push(v),
712 None => res_col.push(Value::Null),
713 }
714 }
715 }
716 }
717
718 result
719 }
720
721 pub fn to_column_major_data_with_snapshot_and_predicate(
724 &self,
725 predicate: Option<(usize, &str, &Value)>,
726 snapshot_ts: Option<u64>,
727 commit_history: &[(u64, u64)],
728 ) -> Vec<Vec<Value>> {
729 let num_cols = self.columns.len();
730 let mut result = vec![Vec::new(); num_cols];
731
732 for group in &self.node_groups {
733 if let Some((col_idx, op, val)) = predicate
734 && let Some(col_chunk) = group.columns.get(col_idx)
735 {
736 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
737 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
738 continue;
739 }
740 }
741
742 for row in 0..group.num_nodes as usize {
743 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
744 match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
745 Some(v) => res_col.push(v),
746 None => res_col.push(Value::Null),
747 }
748 }
749 }
750 }
751
752 result
753 }
754
755 pub fn to_column_major_data_with_predicate_and_ids(
760 &self,
761 predicate: Option<(usize, &str, &Value)>,
762 ) -> (Vec<Vec<Value>>, Vec<u64>) {
763 let num_cols = self.columns.len();
764 let mut result = vec![Vec::new(); num_cols];
765 let mut ids = Vec::new();
766
767 for group in &self.node_groups {
768 if let Some((col_idx, op, val)) = predicate
769 && let Some(col_chunk) = group.columns.get(col_idx)
770 {
771 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
772 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
773 continue;
774 }
775 }
776
777 for row in 0..group.num_nodes as usize {
778 ids.push(group.start_offset + row as u64);
779 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
780 match group.get_value(row, col) {
781 Some(v) => res_col.push(v.clone()),
782 None => res_col.push(Value::Null),
783 }
784 }
785 }
786 }
787
788 (result, ids)
789 }
790
791 pub fn to_column_major_data_with_snapshot_and_predicate_and_ids(
795 &self,
796 predicate: Option<(usize, &str, &Value)>,
797 snapshot_ts: Option<u64>,
798 commit_history: &[(u64, u64)],
799 ) -> (Vec<Vec<Value>>, Vec<u64>) {
800 let num_cols = self.columns.len();
801 let mut result = vec![Vec::new(); num_cols];
802 let mut ids = Vec::new();
803
804 for group in &self.node_groups {
805 if let Some((col_idx, op, val)) = predicate
806 && let Some(col_chunk) = group.columns.get(col_idx)
807 {
808 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
809 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
810 continue;
811 }
812 }
813
814 for row in 0..group.num_nodes as usize {
815 ids.push(group.start_offset + row as u64);
816 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
817 match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
818 Some(v) => res_col.push(v),
819 None => res_col.push(Value::Null),
820 }
821 }
822 }
823 }
824
825 (result, ids)
826 }
827
828 fn find_group(&self, row: u64) -> usize {
830 self.node_groups
831 .binary_search_by_key(&row, |g| g.start_offset)
832 .unwrap_or_else(|i| if i == 0 { 0 } else { i - 1 })
833 }
834}
835
836fn coerce_values_to_columns(values: &mut [Value], columns: &[ColumnDefinition]) -> Result<(), StorageError> {
844 for (i, col) in columns.iter().enumerate() {
845 let coerced = match (col.logical_type, &values[i]) {
846 (LogicalTypeID::UInt64, Value::Int64(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
847 (LogicalTypeID::UInt64, Value::Int32(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
848 (LogicalTypeID::UInt64, Value::Int16(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
849 (LogicalTypeID::UInt64, Value::Int8(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
850 (LogicalTypeID::UInt64, Value::UInt32(x)) => Some(Value::UInt64(*x as u64)),
851 (LogicalTypeID::UInt64, Value::UInt16(x)) => Some(Value::UInt64(*x as u64)),
852 (LogicalTypeID::UInt64, Value::UInt8(x)) => Some(Value::UInt64(*x as u64)),
853 (LogicalTypeID::UInt64, Value::Int64(_))
854 | (LogicalTypeID::UInt64, Value::Int32(_))
855 | (LogicalTypeID::UInt64, Value::Int16(_))
856 | (LogicalTypeID::UInt64, Value::Int8(_)) => {
857 return Err(StorageError::Page(format!(
858 "Cannot store negative value in UINT64 column '{}'",
859 col.name
860 )));
861 }
862 _ => None,
863 };
864 if let Some(c) = coerced {
865 values[i] = c;
866 }
867 }
868 Ok(())
869}
870
871#[derive(Debug, Clone)]
883pub struct RelTable {
884 pub table_id: u64,
885 pub name: String,
886 pub src_table_id: u64,
887 pub dst_table_id: u64,
888 pub columns: Vec<ColumnDefinition>,
889 pub num_rows: u64,
890 pub edges: Vec<(u64, u64)>,
892 pub fwd_adj: HashMap<u64, Vec<(u64, usize)>>,
894 pub rev_adj: HashMap<u64, Vec<(u64, usize)>>,
896 pub csr_index: Option<CsrIndex>,
898 pub properties: Vec<Vec<Value>>,
900 pub persistence_dirty: bool,
903}
904
905impl RelTable {
906 pub fn new(
907 table_id: u64,
908 name: String,
909 src_table_id: u64,
910 dst_table_id: u64,
911 columns: Vec<ColumnDefinition>,
912 ) -> Self {
913 let num_cols = columns.len();
914 Self {
915 table_id,
916 name,
917 src_table_id,
918 dst_table_id,
919 columns,
920 num_rows: 0,
921 edges: Vec::new(),
922 fwd_adj: HashMap::new(),
923 rev_adj: HashMap::new(),
924 csr_index: None,
925 properties: vec![Vec::new(); num_cols],
926 persistence_dirty: false,
927 }
928 }
929
930 pub fn add_column(&mut self, column: ColumnDefinition) {
933 if self.columns.iter().any(|c| c.name.eq_ignore_ascii_case(&column.name)) {
934 return;
935 }
936 self.columns.push(column);
937 self.properties.push(vec![Value::Null; self.edges.len()]);
938 self.persistence_dirty = true;
939 }
940
941 pub fn insert_rel(&mut self, from: u64, to: u64, values: Vec<Value>) -> Result<(), StorageError> {
949 if values.len() != self.columns.len() {
950 return Err(StorageError::Page(format!(
951 "Column count mismatch: expected {} values, got {}",
952 self.columns.len(),
953 values.len()
954 )));
955 }
956
957 let edge_idx = self.edges.len();
958 self.edges.push((from, to));
959
960 self.fwd_adj.entry(from).or_default().push((to, edge_idx));
962
963 self.rev_adj.entry(to).or_default().push((from, edge_idx));
965 for (col_idx, val) in values.into_iter().enumerate() {
967 self.properties[col_idx].push(val);
968 }
969 self.num_rows += 1;
970 Ok(())
971 }
972
973 pub fn insert_rels_batch(&mut self, rels: &[(u64, u64, Vec<Value>)]) -> Result<u64, StorageError> {
976 if rels.is_empty() {
977 return Ok(0);
978 }
979 let num_cols = self.columns.len();
980 let total = rels.len();
981
982 for (i, (_, _, vals)) in rels.iter().enumerate() {
984 if vals.len() != num_cols {
985 return Err(StorageError::Page(format!(
986 "Rel {} column count mismatch: expected {} values, got {}",
987 i,
988 num_cols,
989 vals.len()
990 )));
991 }
992 }
993
994 self.edges.reserve(total);
996 for col in &mut self.properties {
997 col.reserve(total);
998 }
999
1000 let _start_edge_idx = self.edges.len();
1001
1002 for (from, to, vals) in rels {
1004 let edge_idx = self.edges.len();
1005 self.edges.push((*from, *to));
1006 self.fwd_adj.entry(*from).or_default().push((*to, edge_idx));
1007 self.rev_adj.entry(*to).or_default().push((*from, edge_idx));
1008 for (col_idx, val) in vals.iter().enumerate() {
1009 self.properties[col_idx].push(val.clone());
1010 }
1011 }
1012
1013 self.num_rows += total as u64;
1014 Ok(total as u64)
1015 }
1016
1017 pub fn delete_edge(&mut self, edge_idx: usize) -> Result<(), StorageError> {
1020 if edge_idx >= self.edges.len() {
1021 return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
1022 }
1023
1024 let (src, dst) = self.edges[edge_idx];
1025 if src == u64::MAX {
1026 return Ok(());
1028 }
1029
1030 if let Some(adj) = self.fwd_adj.get_mut(&src) {
1032 adj.retain(|&(_, idx)| idx != edge_idx);
1033 }
1034
1035 if let Some(adj) = self.rev_adj.get_mut(&dst) {
1037 adj.retain(|&(_, idx)| idx != edge_idx);
1038 }
1039
1040 self.edges[edge_idx] = (u64::MAX, u64::MAX);
1042 self.persistence_dirty = true;
1043
1044 for col in &mut self.properties {
1046 if edge_idx < col.len() {
1047 col[edge_idx] = Value::Null;
1048 }
1049 }
1050
1051 Ok(())
1052 }
1053
1054 pub fn update_cell(&mut self, edge_idx: usize, col_idx: usize, value: Value) -> Result<(), StorageError> {
1056 if col_idx >= self.columns.len() {
1057 return Err(StorageError::Page(format!("Column index {col_idx} out of range")));
1058 }
1059 if edge_idx >= self.properties[col_idx].len() {
1060 return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
1061 }
1062
1063 self.properties[col_idx][edge_idx] = value;
1064 self.persistence_dirty = true;
1065 Ok(())
1066 }
1067
1068 pub fn edge_undo_bytes(&self, edge_idx: usize) -> Vec<u8> {
1072 let (src, dst) = self.edges.get(edge_idx).copied().unwrap_or((u64::MAX, u64::MAX));
1073 let mut out = Vec::new();
1074 out.extend_from_slice(&Column::serialize_value(&Value::UInt64(src)));
1075 out.extend_from_slice(&Column::serialize_value(&Value::UInt64(dst)));
1076 for p in self.get_edge_properties(edge_idx) {
1077 out.extend_from_slice(&Column::serialize_value(&p));
1078 }
1079 out
1080 }
1081
1082 pub fn edge_cell_undo_bytes(&self, edge_idx: usize, col_idx: usize) -> Vec<u8> {
1085 let v = self
1086 .get_edge_properties(edge_idx)
1087 .get(col_idx)
1088 .cloned()
1089 .unwrap_or(Value::Null);
1090 Column::serialize_value(&v)
1091 }
1092
1093 pub fn restore_deleted_edge(
1097 &mut self,
1098 edge_idx: usize,
1099 src: u64,
1100 dst: u64,
1101 props: Vec<Value>,
1102 ) -> Result<(), StorageError> {
1103 if edge_idx >= self.edges.len() {
1104 return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
1105 }
1106 self.edges[edge_idx] = (src, dst);
1107 self.fwd_adj.entry(src).or_default().push((dst, edge_idx));
1108 self.rev_adj.entry(dst).or_default().push((src, edge_idx));
1109 for (col_idx, val) in props.into_iter().enumerate() {
1110 if col_idx < self.properties.len() {
1111 if edge_idx < self.properties[col_idx].len() {
1112 self.properties[col_idx][edge_idx] = val;
1113 } else {
1114 self.properties[col_idx].push(val);
1115 }
1116 }
1117 }
1118 self.persistence_dirty = true;
1119 Ok(())
1120 }
1121
1122 pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
1126 let num_prop_cols = self.columns.len();
1130 if values.len() != num_prop_cols {
1131 return Err(StorageError::Page(format!(
1132 "Column count mismatch: expected {} values, got {}",
1133 num_prop_cols,
1134 values.len()
1135 )));
1136 }
1137
1138 let from = self.num_rows;
1141 let to = self.num_rows;
1142 self.insert_rel(from, to, values)?;
1143 Ok(0) }
1145
1146 pub fn scan_adj_list(&self, src_offset: u64) -> &[(u64, usize)] {
1151 self.fwd_adj.get(&src_offset).map(|v| v.as_slice()).unwrap_or(&[])
1152 }
1153
1154 pub fn scan_rev_adj_list(&self, dst_offset: u64) -> &[(u64, usize)] {
1159 self.rev_adj.get(&dst_offset).map(|v| v.as_slice()).unwrap_or(&[])
1160 }
1161
1162 pub fn get_outgoing_edges(&self, src_offset: u64) -> Vec<(u64, Vec<Value>)> {
1164 self.scan_adj_list(src_offset)
1165 .iter()
1166 .map(|&(dst, edge_idx)| {
1167 let props = self.get_edge_properties(edge_idx);
1168 (dst, props)
1169 })
1170 .collect()
1171 }
1172
1173 pub fn get_incoming_edges(&self, dst_offset: u64) -> Vec<(u64, Vec<Value>)> {
1175 self.scan_rev_adj_list(dst_offset)
1176 .iter()
1177 .map(|&(src, edge_idx)| {
1178 let props = self.get_edge_properties(edge_idx);
1179 (src, props)
1180 })
1181 .collect()
1182 }
1183
1184 pub fn get_edge_properties(&self, edge_idx: usize) -> Vec<Value> {
1186 let mut props = Vec::with_capacity(self.columns.len());
1187 for col in &self.properties {
1188 match col.get(edge_idx) {
1189 Some(v) => props.push(v.clone()),
1190 None => props.push(Value::Null),
1191 }
1192 }
1193 props
1194 }
1195
1196 pub fn get_column(&self, col_idx: usize) -> Option<&[Value]> {
1198 self.properties.get(col_idx).map(|v| v.as_slice())
1199 }
1200
1201 pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
1203 self.properties.clone()
1204 }
1205}
1206
1207#[derive(Debug, Default)]
1214pub struct TableCatalog {
1215 node_tables: DashMap<u64, NodeTable>,
1216 rel_tables: DashMap<u64, RelTable>,
1217 vector_indexes: DashMap<u64, VectorIndexTable>,
1218 node_name_to_id: DashMap<String, u64>,
1220 rel_name_to_id: DashMap<String, u64>,
1222 vector_index_name_to_id: DashMap<String, u64>,
1224 next_table_id: std::sync::atomic::AtomicU64,
1225}
1226
1227impl TableCatalog {
1228 pub fn new() -> Self {
1229 Self::default()
1230 }
1231
1232 pub fn create_node_table(&self, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1233 let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1234 let table = NodeTable::new(table_id, name.clone(), columns);
1235 self.node_name_to_id.insert(name, table_id);
1236 self.node_tables.insert(table_id, table.clone());
1237 table
1238 }
1239
1240 pub fn create_node_table_with_id(&self, table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1244 self.bump_next_table_id(table_id);
1245 let table = NodeTable::new(table_id, name.clone(), columns);
1246 self.node_name_to_id.insert(name, table_id);
1247 self.node_tables.insert(table_id, table.clone());
1248 table
1249 }
1250
1251 pub fn create_rel_table(
1252 &self,
1253 name: String,
1254 src_table_id: u64,
1255 dst_table_id: u64,
1256 columns: Vec<ColumnDefinition>,
1257 ) -> RelTable {
1258 let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1259 let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1260 self.rel_name_to_id.insert(name, table_id);
1261 self.rel_tables.insert(table_id, table.clone());
1262 table
1263 }
1264
1265 pub fn create_rel_table_with_id(
1269 &self,
1270 table_id: u64,
1271 name: String,
1272 src_table_id: u64,
1273 dst_table_id: u64,
1274 columns: Vec<ColumnDefinition>,
1275 ) -> RelTable {
1276 self.bump_next_table_id(table_id);
1277 let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1278 self.rel_name_to_id.insert(name, table_id);
1279 self.rel_tables.insert(table_id, table.clone());
1280 table
1281 }
1282
1283 fn bump_next_table_id(&self, table_id: u64) {
1286 let mut next = self.next_table_id.load(std::sync::atomic::Ordering::SeqCst);
1287 while next <= table_id {
1288 match self.next_table_id.compare_exchange(
1289 next,
1290 table_id + 1,
1291 std::sync::atomic::Ordering::SeqCst,
1292 std::sync::atomic::Ordering::SeqCst,
1293 ) {
1294 Ok(_) => break,
1295 Err(current) => next = current,
1296 }
1297 }
1298 }
1299
1300 pub fn get_node_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1301 self.node_tables.get(&table_id)
1302 }
1303
1304 pub fn get_node_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1305 self.node_tables.get_mut(&table_id)
1306 }
1307
1308 pub fn get_node_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1309 let id = self.node_name_to_id.get(name)?;
1310 self.node_tables.get(&*id)
1311 }
1312
1313 pub fn get_node_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1314 let id = self.node_name_to_id.get(name)?;
1315 self.node_tables.get_mut(&*id)
1316 }
1317
1318 pub fn get_rel_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1319 self.rel_tables.get(&table_id)
1320 }
1321
1322 pub fn get_rel_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1323 self.rel_tables.get_mut(&table_id)
1324 }
1325
1326 pub fn get_rel_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1327 let id = self.rel_name_to_id.get(name)?;
1328 self.rel_tables.get(&*id)
1329 }
1330
1331 pub fn get_rel_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1332 let id = self.rel_name_to_id.get(name)?;
1333 self.rel_tables.get_mut(&*id)
1334 }
1335
1336 pub fn has_incident_edges(&self, table_id: u64, node_idx: u64) -> bool {
1338 for rel_table in self.rel_tables.iter() {
1339 if rel_table.src_table_id == table_id {
1340 if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1341 if !edges.is_empty() {
1342 return true;
1343 }
1344 }
1345 }
1346 if rel_table.dst_table_id == table_id {
1347 if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1348 if !edges.is_empty() {
1349 return true;
1350 }
1351 }
1352 }
1353 }
1354 false
1355 }
1356
1357 pub fn detach_node(&self, table_id: u64, node_idx: u64) {
1359 for mut rel_table in self.rel_tables.iter_mut() {
1360 let mut edges_to_delete = Vec::new();
1361
1362 if rel_table.src_table_id == table_id {
1363 if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1364 for &(_, edge_idx) in edges {
1365 edges_to_delete.push(edge_idx);
1366 }
1367 }
1368 }
1369
1370 if rel_table.dst_table_id == table_id {
1371 if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1372 for &(_, edge_idx) in edges {
1373 edges_to_delete.push(edge_idx);
1374 }
1375 }
1376 }
1377
1378 for edge_idx in edges_to_delete {
1379 let _ = rel_table.delete_edge(edge_idx);
1380 }
1381 }
1382 }
1383
1384 pub fn all_node_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, NodeTable>> {
1385 self.node_tables.iter().collect()
1386 }
1387
1388 pub fn all_rel_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, RelTable>> {
1389 self.rel_tables.iter().collect()
1390 }
1391
1392 pub fn node_table_num_rows(&self, name: &str) -> u64 {
1394 self.get_node_table_by_name(name).map(|t| t.num_rows).unwrap_or(0)
1395 }
1396
1397 pub fn drop_node_table(&self, name: &str) -> bool {
1399 if let Some(id) = self.node_name_to_id.get(name) {
1400 let table_id = *id;
1401 drop(id);
1402 self.node_name_to_id.remove(name);
1403 self.node_tables.remove(&table_id).is_some()
1404 } else {
1405 false
1406 }
1407 }
1408
1409 pub fn create_vector_index(
1411 &self,
1412 name: String,
1413 table_name: String,
1414 column_name: String,
1415 metric: DistanceMetric,
1416 dimensions: u32,
1417 ) -> VectorIndexTable {
1418 let index_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1419 let table = VectorIndexTable::new(index_id, name.clone(), table_name, column_name, metric, dimensions);
1420 self.vector_index_name_to_id.insert(name, index_id);
1421 self.vector_indexes.insert(index_id, table.clone());
1422 table
1423 }
1424
1425 pub fn get_vector_index(&self, index_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1427 self.vector_indexes.get(&index_id)
1428 }
1429
1430 pub fn get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1432 let id = self.vector_index_name_to_id.get(name)?;
1433 self.vector_indexes.get(&*id)
1434 }
1435
1436 pub fn get_vector_index_by_name_mut(
1438 &self,
1439 name: &str,
1440 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1441 let id = self.vector_index_name_to_id.get(name)?;
1442 self.vector_indexes.get_mut(&*id)
1443 }
1444
1445 pub fn get_vector_index_mut(
1447 &self,
1448 index_id: u64,
1449 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1450 self.vector_indexes.get_mut(&index_id)
1451 }
1452
1453 pub fn drop_vector_index(&self, name: &str) -> bool {
1455 if let Some(id) = self.vector_index_name_to_id.get(name) {
1456 let index_id = *id;
1457 drop(id);
1458 self.vector_index_name_to_id.remove(name);
1459 self.vector_indexes.remove(&index_id).is_some()
1460 } else {
1461 false
1462 }
1463 }
1464
1465 pub fn all_vector_indexes(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, VectorIndexTable>> {
1467 self.vector_indexes.iter().collect()
1468 }
1469
1470 pub fn refresh_vector_index(&self, index_id: u64) {
1475 let (table_name, column_name) = match self.vector_indexes.get(&index_id) {
1476 Some(vi) => (vi.table_name.clone(), vi.column_name.clone()),
1477 None => return,
1478 };
1479
1480 let col_idx = match self.get_node_table_by_name(&table_name) {
1481 Some(t) => match t.columns.iter().position(|c| c.name == column_name) {
1482 Some(idx) => idx,
1483 None => return,
1484 },
1485 None => return,
1486 };
1487
1488 let mut data: Vec<(usize, Vec<f64>)> = Vec::new();
1491 if let Some(table) = self.get_node_table_by_name(&table_name) {
1492 for row_id in 0..table.num_rows as usize {
1493 if let Some(val) = table.get_value(row_id, col_idx) {
1494 if let Ok(vec) = crate::extract_f64_list_from_value(val) {
1495 data.push((row_id, vec));
1496 }
1497 }
1498 }
1499 }
1500 if data.is_empty() {
1501 if let Some(mut vi) = self.vector_indexes.get_mut(&index_id) {
1502 vi.hnsw_mut().clear();
1503 }
1504 return;
1505 }
1506
1507 let mut vi = self.vector_indexes.get_mut(&index_id);
1508 if let Some(vi) = vi.as_mut() {
1509 vi.hnsw_mut().clear();
1510 for (row_id, vec) in data {
1511 vi.hnsw_mut().insert(vec, row_id);
1512 }
1513 }
1514 }
1515
1516 pub fn refresh_vector_indexes_for_tables(&self, table_ids: &[u64]) {
1521 for table_id in table_ids {
1522 let table_name = match self.get_node_table(*table_id) {
1523 Some(t) => t.name.clone(),
1524 None => continue,
1525 };
1526 let index_ids: Vec<u64> = self
1527 .vector_indexes
1528 .iter()
1529 .filter(|vi| vi.table_name == table_name)
1530 .map(|vi| *vi.key())
1531 .collect();
1532 for index_id in index_ids {
1533 self.refresh_vector_index(index_id);
1534 }
1535 }
1536 }
1537
1538 pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
1545 let mut table = self
1546 .get_node_table_by_name_mut(table_name)
1547 .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1548
1549 if table.art_index.is_some() {
1550 return Err(StorageError::Index(format!(
1551 "Table '{table_name}' already has an ART index"
1552 )));
1553 }
1554
1555 let mut art_idx = ArtPrimaryKeyIndex::new(index_name);
1556
1557 let pk_col = table.primary_key_column;
1559 let col_major = table.to_column_major_data();
1561 if pk_col < col_major.len() {
1562 for (row_offset, pk_val) in col_major[pk_col].iter().enumerate() {
1563 if !matches!(pk_val, Value::Null)
1564 && let Some(art_key) = ArtKey::from_value(pk_val)
1565 {
1566 art_idx.insert(&art_key, row_offset as u64);
1567 }
1568 }
1569 }
1570
1571 table.art_index = Some(art_idx);
1572 Ok(())
1573 }
1574
1575 pub fn drop_art_index(&self, table_name: &str) -> Result<(), StorageError> {
1577 let mut table = self
1578 .get_node_table_by_name_mut(table_name)
1579 .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1580
1581 table.art_index = None;
1582 Ok(())
1583 }
1584
1585 pub fn get_art_index(&self, table_name: &str) -> Option<ArtPrimaryKeyIndex> {
1588 let table = self.get_node_table_by_name(table_name)?;
1589 table.art_index.clone()
1590 }
1591
1592 pub fn has_art_index(&self, table_name: &str) -> bool {
1594 self.get_node_table_by_name(table_name)
1595 .map(|t| t.art_index.is_some())
1596 .unwrap_or(false)
1597 }
1598
1599 pub fn drop_rel_table(&self, name: &str) -> bool {
1601 if let Some(id) = self.rel_name_to_id.get(name) {
1602 let table_id = *id;
1603 drop(id);
1604 self.rel_name_to_id.remove(name);
1605 self.rel_tables.remove(&table_id).is_some()
1606 } else {
1607 false
1608 }
1609 }
1610}
1611
1612#[cfg(test)]
1617mod tests {
1618 use super::*;
1619 use crate::column_chunk::NODE_GROUP_SIZE;
1620 use std::sync::Arc;
1621
1622 #[test]
1625 fn test_node_table_empty() {
1626 let table = NodeTable::new(
1627 1,
1628 "Person".into(),
1629 vec![
1630 ColumnDefinition {
1631 compression: akar_common::enums::CompressionType::Uncompressed,
1632 name: "name".into(),
1633 logical_type: LogicalTypeID::String,
1634 is_primary_key: true,
1635 },
1636 ColumnDefinition {
1637 compression: akar_common::enums::CompressionType::Uncompressed,
1638 name: "age".into(),
1639 logical_type: LogicalTypeID::Int64,
1640 is_primary_key: false,
1641 },
1642 ],
1643 );
1644 assert_eq!(table.num_rows, 0);
1645 assert!(table.node_groups.is_empty());
1646 }
1647
1648 #[test]
1649 fn test_node_table_insert_and_get() {
1650 let mut table = NodeTable::new(
1651 1,
1652 "Person".into(),
1653 vec![
1654 ColumnDefinition {
1655 compression: akar_common::enums::CompressionType::Uncompressed,
1656 name: "name".into(),
1657 logical_type: LogicalTypeID::String,
1658 is_primary_key: true,
1659 },
1660 ColumnDefinition {
1661 compression: akar_common::enums::CompressionType::Uncompressed,
1662 name: "age".into(),
1663 logical_type: LogicalTypeID::Int64,
1664 is_primary_key: false,
1665 },
1666 ],
1667 );
1668 table
1669 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1670 .unwrap();
1671 table
1672 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1673 .unwrap();
1674
1675 assert_eq!(table.num_rows, 2);
1676 assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1677 assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1678 }
1679
1680 #[test]
1681 fn test_node_table_batch_insert_with_spiller_restores_all_rows() {
1682 let dir = tempfile::tempdir().unwrap();
1683 let spiller = Arc::new(crate::spiller::Spiller::new(dir.path(), 64));
1684 let mut table = NodeTable::new(
1685 1,
1686 "T".into(),
1687 vec![ColumnDefinition {
1688 compression: akar_common::enums::CompressionType::Uncompressed,
1689 name: "id".into(),
1690 logical_type: LogicalTypeID::Int64,
1691 is_primary_key: true,
1692 }],
1693 );
1694 table.set_spiller(Some(spiller));
1695
1696 let rows: Vec<Vec<Value>> = (0..2000).map(|i| vec![Value::Int64(i)]).collect();
1697 let inserted = table.insert_rows_batch(&rows).unwrap();
1698 assert_eq!(inserted, 2000);
1699 assert_eq!(table.num_rows, 2000);
1700
1701 for i in 0i64..2000 {
1702 assert_eq!(table.get_value(i as usize, 0), Some(&Value::Int64(i)));
1703 }
1704 assert!(
1705 table.node_groups.iter().all(|g| !g.has_spill_files()),
1706 "spill files must be merged back after the batch"
1707 );
1708 }
1709
1710 #[test]
1711 fn test_delete_row_removes_pk_from_hash_and_art_index() {
1712 let mut table = NodeTable::new(
1713 1,
1714 "Person".into(),
1715 vec![
1716 ColumnDefinition {
1717 compression: akar_common::enums::CompressionType::Uncompressed,
1718 name: "name".into(),
1719 logical_type: LogicalTypeID::String,
1720 is_primary_key: true,
1721 },
1722 ColumnDefinition {
1723 compression: akar_common::enums::CompressionType::Uncompressed,
1724 name: "age".into(),
1725 logical_type: LogicalTypeID::Int64,
1726 is_primary_key: false,
1727 },
1728 ],
1729 );
1730 table.art_index = Some(ArtPrimaryKeyIndex::new("test_art"));
1731 table
1732 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1733 .unwrap();
1734 table
1735 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1736 .unwrap();
1737 let art = table.art_index.as_ref().unwrap();
1738 assert_eq!(
1739 art.lookup(&ArtKey::from_value(&Value::String("Alice".into())).unwrap()),
1740 Some(0)
1741 );
1742
1743 table.delete_row(0).unwrap();
1744
1745 assert!(table.lookup_by_pk(&Value::String("Alice".into())).is_none());
1747 let art = table.art_index.as_ref().unwrap();
1748 assert_eq!(art.len(), 1, "ART must drop the deleted entry");
1749 assert!(
1750 art.lookup(&ArtKey::from_value(&Value::String("Alice".into())).unwrap())
1751 .is_none()
1752 );
1753 assert!(
1754 art.lookup(&ArtKey::from_value(&Value::String("Bob".into())).unwrap())
1755 .is_some()
1756 );
1757 let hits = table.lookup_by_pk_range(
1759 Some(&Value::String("A".into())),
1760 true,
1761 Some(&Value::String("C".into())),
1762 true,
1763 100,
1764 );
1765 assert_eq!(hits, vec![1], "only 'Bob' (row 1) should be in range");
1766
1767 table
1769 .insert_row(vec![Value::String("Alice".into()), Value::Int64(31)])
1770 .unwrap();
1771 assert_eq!(table.lookup_by_pk(&Value::String("Alice".into())), Some(2));
1772 }
1773
1774 #[test]
1775 fn test_node_table_scan_column() {
1776 let mut table = NodeTable::new(
1777 1,
1778 "T".into(),
1779 vec![ColumnDefinition {
1780 compression: akar_common::enums::CompressionType::Uncompressed,
1781 name: "val".into(),
1782 logical_type: LogicalTypeID::Int64,
1783 is_primary_key: false,
1784 }],
1785 );
1786 for i in 0..100 {
1787 table.insert_row(vec![Value::Int64(i)]).unwrap();
1788 }
1789 let scanned = table.scan_column(0, 10, 5, None, &[]);
1790 assert_eq!(scanned.len(), 5);
1791 assert_eq!(scanned[0], Value::Int64(10));
1792 assert_eq!(scanned[4], Value::Int64(14));
1793 }
1794
1795 #[test]
1796 fn test_node_table_to_column_major() {
1797 let mut table = NodeTable::new(
1798 1,
1799 "T".into(),
1800 vec![
1801 ColumnDefinition {
1802 compression: akar_common::enums::CompressionType::Uncompressed,
1803 name: "x".into(),
1804 logical_type: LogicalTypeID::Int64,
1805 is_primary_key: false,
1806 },
1807 ColumnDefinition {
1808 compression: akar_common::enums::CompressionType::Uncompressed,
1809 name: "y".into(),
1810 logical_type: LogicalTypeID::Int64,
1811 is_primary_key: false,
1812 },
1813 ],
1814 );
1815 table.insert_row(vec![Value::Int64(1), Value::Int64(10)]).unwrap();
1816 table.insert_row(vec![Value::Int64(2), Value::Int64(20)]).unwrap();
1817
1818 let data = table.to_column_major_data();
1819 assert_eq!(data.len(), 2);
1820 assert_eq!(data[0], vec![Value::Int64(1), Value::Int64(2)]);
1821 assert_eq!(data[1], vec![Value::Int64(10), Value::Int64(20)]);
1822 }
1823
1824 #[test]
1825 fn test_node_table_auto_node_group() {
1826 let mut table = NodeTable::new(
1827 1,
1828 "T".into(),
1829 vec![ColumnDefinition {
1830 compression: akar_common::enums::CompressionType::Uncompressed,
1831 name: "v".into(),
1832 logical_type: LogicalTypeID::Int64,
1833 is_primary_key: false,
1834 }],
1835 );
1836 for i in 0..NODE_GROUP_SIZE as u64 + 1 {
1838 table.insert_row(vec![Value::Int64(i as i64)]).unwrap();
1839 }
1840 assert_eq!(table.num_rows, NODE_GROUP_SIZE as u64 + 1);
1841 assert_eq!(table.node_groups.len(), 2);
1842 assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1843 assert_eq!(table.node_groups[1].num_nodes, 1);
1844 assert_eq!(table.get_value(0, 0), Some(&Value::Int64(0)));
1846 assert_eq!(
1847 table.get_value(NODE_GROUP_SIZE, 0),
1848 Some(&Value::Int64(NODE_GROUP_SIZE as i64))
1849 );
1850 }
1851
1852 fn make_rel_table() -> RelTable {
1855 RelTable::new(
1856 1,
1857 "Knows".into(),
1858 0,
1859 1,
1860 vec![
1861 ColumnDefinition {
1862 compression: akar_common::enums::CompressionType::Uncompressed,
1863 name: "since".into(),
1864 logical_type: LogicalTypeID::Int64,
1865 is_primary_key: false,
1866 },
1867 ColumnDefinition {
1868 compression: akar_common::enums::CompressionType::Uncompressed,
1869 name: "weight".into(),
1870 logical_type: LogicalTypeID::Double,
1871 is_primary_key: false,
1872 },
1873 ],
1874 )
1875 }
1876
1877 #[test]
1878 fn test_rel_table_empty() {
1879 let rel = make_rel_table();
1880 assert_eq!(rel.num_rows, 0);
1881 assert!(rel.edges.is_empty());
1882 assert!(rel.fwd_adj.is_empty());
1883 assert!(rel.rev_adj.is_empty());
1884 }
1885
1886 #[test]
1887 fn test_rel_insert_basic() {
1888 let mut rel = make_rel_table();
1889 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1890 .unwrap();
1891 rel.insert_rel(0, 2, vec![Value::Int64(2021), Value::Double(0.8)])
1892 .unwrap();
1893 rel.insert_rel(1, 0, vec![Value::Int64(2020), Value::Double(0.3)])
1894 .unwrap();
1895
1896 assert_eq!(rel.num_rows, 3);
1897 assert_eq!(rel.edges.len(), 3);
1898
1899 let fwd = rel.scan_adj_list(0);
1901 assert_eq!(fwd.len(), 2);
1902 assert_eq!(fwd[0], (1, 0)); assert_eq!(fwd[1], (2, 1)); let fwd1 = rel.scan_adj_list(1);
1907 assert_eq!(fwd1.len(), 1);
1908 assert_eq!(fwd1[0], (0, 2));
1909 }
1910
1911 #[test]
1912 fn test_rel_reverse_adjacency() {
1913 let mut rel = make_rel_table();
1914 rel.insert_rel(0, 5, vec![Value::Int64(2022), Value::Double(1.0)])
1915 .unwrap();
1916 rel.insert_rel(3, 5, vec![Value::Int64(2023), Value::Double(1.5)])
1917 .unwrap();
1918
1919 let rev = rel.scan_rev_adj_list(5);
1921 assert_eq!(rev.len(), 2);
1922 assert_eq!(rev[0], (0, 0));
1923 assert_eq!(rev[1], (3, 1));
1924 }
1925
1926 #[test]
1927 fn test_rel_get_edge_properties() {
1928 let mut rel = make_rel_table();
1929 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1930 .unwrap();
1931 rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
1932 .unwrap();
1933
1934 let props0 = rel.get_edge_properties(0);
1935 assert_eq!(props0, vec![Value::Int64(2020), Value::Double(0.5)]);
1936
1937 let props1 = rel.get_edge_properties(1);
1938 assert_eq!(props1, vec![Value::Int64(2021), Value::Double(0.9)]);
1939 }
1940
1941 #[test]
1942 fn test_rel_get_outgoing_edges() {
1943 let mut rel = make_rel_table();
1944 rel.insert_rel(0, 10, vec![Value::Int64(2020), Value::Double(1.0)])
1945 .unwrap();
1946 rel.insert_rel(0, 20, vec![Value::Int64(2021), Value::Double(2.0)])
1947 .unwrap();
1948
1949 let outgoing = rel.get_outgoing_edges(0);
1950 assert_eq!(outgoing.len(), 2);
1951 assert_eq!(outgoing[0].0, 10);
1952 assert_eq!(outgoing[0].1, vec![Value::Int64(2020), Value::Double(1.0)]);
1953 assert_eq!(outgoing[1].0, 20);
1954 }
1955
1956 #[test]
1957 fn test_rel_get_incoming_edges() {
1958 let mut rel = make_rel_table();
1959 rel.insert_rel(10, 5, vec![Value::Int64(2020), Value::Double(1.0)])
1960 .unwrap();
1961 rel.insert_rel(20, 5, vec![Value::Int64(2021), Value::Double(2.0)])
1962 .unwrap();
1963
1964 let incoming = rel.get_incoming_edges(5);
1965 assert_eq!(incoming.len(), 2);
1966 assert_eq!(incoming[0].0, 10);
1967 assert_eq!(incoming[1].0, 20);
1968 }
1969
1970 #[test]
1971 fn test_rel_no_edges() {
1972 let rel = make_rel_table();
1973 assert!(rel.scan_adj_list(0).is_empty());
1974 assert!(rel.scan_rev_adj_list(0).is_empty());
1975 assert!(rel.get_outgoing_edges(0).is_empty());
1976 assert!(rel.get_incoming_edges(0).is_empty());
1977 }
1978
1979 #[test]
1980 fn test_rel_insert_row_legacy() {
1981 let mut rel = make_rel_table();
1982 rel.insert_row(vec![Value::Int64(2022), Value::Double(3.0)]).unwrap();
1984 assert_eq!(rel.num_rows, 1);
1985 assert_eq!(rel.edges[0], (0, 0)); assert_eq!(rel.get_edge_properties(0), vec![Value::Int64(2022), Value::Double(3.0)]);
1987 }
1988
1989 #[test]
1990 fn test_rel_wrong_column_count() {
1991 let mut rel = make_rel_table();
1992 let result = rel.insert_rel(0, 1, vec![Value::Int64(42)]); assert!(result.is_err());
1994 }
1995
1996 #[test]
1997 fn test_rel_get_column() {
1998 let mut rel = make_rel_table();
1999 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(1.5)])
2000 .unwrap();
2001 rel.insert_rel(1, 2, vec![Value::Int64(2021), Value::Double(2.5)])
2002 .unwrap();
2003
2004 let since_col = rel.get_column(0).unwrap();
2005 assert_eq!(since_col, &[Value::Int64(2020), Value::Int64(2021)]);
2006
2007 let weight_col = rel.get_column(1).unwrap();
2008 assert_eq!(weight_col, &[Value::Double(1.5), Value::Double(2.5)]);
2009 }
2010
2011 #[test]
2012 fn test_rel_to_column_major() {
2013 let mut rel = make_rel_table();
2014 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
2015 .unwrap();
2016 rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
2017 .unwrap();
2018
2019 let data = rel.to_column_major_data();
2020 assert_eq!(data.len(), 2);
2021 assert_eq!(data[0], vec![Value::Int64(2020), Value::Int64(2021)]);
2022 assert_eq!(data[1], vec![Value::Double(0.5), Value::Double(0.9)]);
2023 }
2024
2025 #[test]
2028 fn test_catalog_create_and_lookup() {
2029 let cat = TableCatalog::new();
2030 let node_table = cat.create_node_table(
2031 "Person".into(),
2032 vec![ColumnDefinition {
2033 compression: akar_common::enums::CompressionType::Uncompressed,
2034 name: "id".into(),
2035 logical_type: LogicalTypeID::Int64,
2036 is_primary_key: true,
2037 }],
2038 );
2039 assert_eq!(node_table.table_id, 0);
2040
2041 let rel_table = cat.create_rel_table(
2042 "Knows".into(),
2043 0,
2044 1,
2045 vec![ColumnDefinition {
2046 compression: akar_common::enums::CompressionType::Uncompressed,
2047 name: "since".into(),
2048 logical_type: LogicalTypeID::Int64,
2049 is_primary_key: false,
2050 }],
2051 );
2052 assert_eq!(rel_table.table_id, 1);
2053
2054 assert!(cat.get_node_table(0).is_some());
2055 assert!(cat.get_rel_table(1).is_some());
2056 assert_eq!(cat.node_table_num_rows("Person"), 0);
2057 }
2058
2059 #[test]
2060 fn test_refresh_vector_index_after_dml() {
2061 let cat = TableCatalog::new();
2065 cat.create_node_table(
2066 "Item".into(),
2067 vec![
2068 ColumnDefinition {
2069 compression: akar_common::enums::CompressionType::Uncompressed,
2070 name: "id".into(),
2071 logical_type: LogicalTypeID::Int64,
2072 is_primary_key: true,
2073 },
2074 ColumnDefinition {
2075 compression: akar_common::enums::CompressionType::Uncompressed,
2076 name: "embedding".into(),
2077 logical_type: LogicalTypeID::List,
2078 is_primary_key: false,
2079 },
2080 ],
2081 );
2082
2083 let vec3 = |x: f64, y: f64, z: f64| Value::List(vec![Value::Double(x), Value::Double(y), Value::Double(z)]);
2084 {
2085 let mut t = cat.get_node_table_by_name_mut("Item").unwrap();
2086 t.insert_row(vec![Value::Int64(1), vec3(1.0, 0.0, 0.0)]).unwrap();
2087 t.insert_row(vec![Value::Int64(2), vec3(0.0, 1.0, 0.0)]).unwrap();
2088 t.insert_row(vec![Value::Int64(3), vec3(0.0, 0.0, 1.0)]).unwrap();
2089 }
2090
2091 cat.create_vector_index(
2092 "item_vec".into(),
2093 "Item".into(),
2094 "embedding".into(),
2095 DistanceMetric::Cosine,
2096 3,
2097 );
2098
2099 cat.refresh_vector_indexes_for_tables(&[0]);
2101 {
2102 let vi = cat.get_vector_index(1).unwrap();
2103 assert_eq!(vi.hnsw().len(), 3);
2104 assert!(vi.hnsw().get_vector(2).is_some());
2105 }
2106
2107 {
2109 let mut t = cat.get_node_table_by_name_mut("Item").unwrap();
2110 t.insert_row(vec![Value::Int64(4), vec3(1.0, 1.0, 0.0)]).unwrap();
2111 t.insert_row(vec![Value::Int64(5), vec3(0.0, 1.0, 1.0)]).unwrap();
2112 }
2113
2114 cat.refresh_vector_indexes_for_tables(&[0]);
2115 let vi = cat.get_vector_index(1).unwrap();
2116 assert_eq!(vi.hnsw().len(), 5);
2117 assert!(vi.hnsw().get_vector(4).is_some());
2118 }
2119}