1use crate::art_index::ArtPrimaryKeyIndex;
4use crate::art_key::ArtKey;
5use crate::csr::CsrIndex;
6use crate::index::HashIndex;
7use crate::node_group::NodeGroup;
8use crate::vector_index::VectorIndexTable;
9use akar_common::error::StorageError;
10use akar_common::types::{LogicalTypeID, Value};
11use akar_vector::hnsw::DistanceMetric;
12use dashmap::DashMap;
13use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
17pub struct ColumnDefinition {
18 pub name: String,
19 pub logical_type: LogicalTypeID,
20 pub is_primary_key: bool,
21 pub compression: akar_common::enums::CompressionType,
22}
23
24#[derive(Debug, Clone)]
35pub struct NodeTable {
36 pub table_id: u64,
37 pub name: String,
38 pub columns: Vec<ColumnDefinition>,
39 pub primary_key_column: usize,
40 pub num_rows: u64,
41 pub node_groups: Vec<NodeGroup>,
44 pub hash_index: HashIndex<String>,
47 pub art_index: Option<ArtPrimaryKeyIndex>,
50 pub persistence_dirty: bool,
53}
54
55pub const NO_PRIMARY_KEY: usize = usize::MAX;
61
62impl NodeTable {
63 pub fn new(table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> Self {
64 let primary_key_column = columns.iter().position(|c| c.is_primary_key).unwrap_or(NO_PRIMARY_KEY);
69 Self {
70 table_id,
71 name,
72 columns,
73 primary_key_column,
74 num_rows: 0,
75 node_groups: Vec::new(),
76 hash_index: HashIndex::new(),
77 art_index: None,
78 persistence_dirty: false,
79 }
80 }
81
82 pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
97 self.insert_row_with_txn(values, None)
98 }
99
100 pub fn insert_row_with_txn(&mut self, mut values: Vec<Value>, txn_id: Option<u64>) -> Result<u64, StorageError> {
102 if values.len() != self.columns.len() {
103 return Err(StorageError::Page(format!(
104 "Column count mismatch: expected {} values, got {}",
105 self.columns.len(),
106 values.len()
107 )));
108 }
109
110 if self.primary_key_column < self.columns.len() {
112 let pk_value = &values[self.primary_key_column];
113 if matches!(pk_value, Value::Null) {
114 return Err(StorageError::Page(format!(
115 "NULL value not allowed for primary key column '{}' in table '{}'",
116 self.columns[self.primary_key_column].name, self.name
117 )));
118 }
119 }
120
121 coerce_values_to_columns(&mut values, &self.columns)?;
126
127 if self.primary_key_column < self.columns.len() {
129 let pk_value = &values[self.primary_key_column];
130 let pk_key = pk_value_to_string(pk_value);
131 if self.hash_index.lookup(&pk_key).is_some() {
132 return Err(StorageError::Index(format!(
133 "Duplicate primary key value: '{pk_key}' in table '{}'",
134 self.name
135 )));
136 }
137 }
138
139 let num_cols = self.columns.len();
141 if self.node_groups.is_empty() || self.node_groups.last().unwrap().is_full() {
142 let start_offset = self.num_rows;
143 let mut new_group = NodeGroup::new(num_cols, start_offset);
144 if txn_id.is_some() {
146 new_group.enable_version_info();
147 }
148 self.node_groups.push(new_group);
149 }
150
151 let current = self.node_groups.last_mut().unwrap();
152 if txn_id.is_some() {
154 current.enable_version_info();
155 }
156 current.append_row_with_txn(values.clone(), txn_id)?;
157 self.num_rows += 1;
158
159 if self.primary_key_column < self.columns.len() {
161 let pk_value = &values[self.primary_key_column];
162 let pk_key = pk_value_to_string(pk_value);
163 self.hash_index.insert(pk_key, self.num_rows - 1);
164
165 if let Some(ref mut art_idx) = self.art_index
167 && let Some(art_key) = ArtKey::from_value(pk_value)
168 {
169 art_idx.insert(&art_key, self.num_rows - 1);
170 }
171 }
172
173 Ok(self.num_rows - 1)
174 }
175
176 pub fn insert_rows_batch(&mut self, rows: &[Vec<Value>]) -> Result<u64, StorageError> {
180 self.insert_rows_batch_with_txn(rows, None)
181 }
182
183 pub fn insert_rows_batch_with_txn(
185 &mut self,
186 rows: &[Vec<Value>],
187 txn_id: Option<u64>,
188 ) -> Result<u64, StorageError> {
189 if rows.is_empty() {
190 return Ok(0);
191 }
192 let num_cols = self.columns.len();
193
194 for (i, row) in rows.iter().enumerate() {
196 if row.len() != num_cols {
197 return Err(StorageError::Page(format!(
198 "Row {} column count mismatch: expected {} values, got {}",
199 i,
200 num_cols,
201 row.len()
202 )));
203 }
204 if self.primary_key_column < num_cols {
206 let pk_value = &row[self.primary_key_column];
207 if matches!(pk_value, Value::Null) {
208 return Err(StorageError::Page(format!(
209 "NULL value not allowed for primary key column '{}' in table '{}'",
210 self.columns[self.primary_key_column].name, self.name
211 )));
212 }
213 }
214 if self.primary_key_column < num_cols {
216 let pk_key = pk_value_to_string(&row[self.primary_key_column]);
217 if self.hash_index.lookup(&pk_key).is_some() {
218 return Err(StorageError::Index(format!(
219 "Duplicate primary key value: '{pk_key}' in table '{}'",
220 self.name
221 )));
222 }
223 }
224 }
225
226 let start_offset = self.num_rows;
227 let total_new = rows.len();
228
229 let mut coerced_rows = rows.to_vec();
233 for row in &mut coerced_rows {
234 coerce_values_to_columns(row, &self.columns)?;
235 }
236 let rows: &[Vec<Value>] = &coerced_rows;
237
238 if self.node_groups.is_empty() || self.node_groups.last().unwrap().is_full() {
240 let off = if self.node_groups.is_empty() {
241 start_offset
242 } else {
243 self.num_rows
244 };
245 let mut new_group = NodeGroup::new(num_cols, off);
246 if txn_id.is_some() {
247 new_group.enable_version_info();
248 }
249 self.node_groups.push(new_group);
250 }
251
252 let mut inserted = 0usize;
254 while inserted < total_new {
255 let current = self.node_groups.last_mut().unwrap();
256 if txn_id.is_some() {
257 current.enable_version_info();
258 }
259 let rem = current.remaining();
260 let take = (total_new - inserted).min(rem);
261 for row in &rows[inserted..inserted + take] {
262 current.append_row_with_txn(row.clone(), txn_id)?;
263 }
264 self.num_rows += take as u64;
265 inserted += take;
266 if inserted < total_new {
267 let off = self.num_rows;
268 let mut new_group = NodeGroup::new(num_cols, off);
269 if txn_id.is_some() {
270 new_group.enable_version_info();
271 }
272 self.node_groups.push(new_group);
273 }
274 }
275
276 for (i, row) in rows.iter().enumerate() {
278 if self.primary_key_column < num_cols {
279 let pk_key = pk_value_to_string(&row[self.primary_key_column]);
280 self.hash_index.insert(pk_key, start_offset + i as u64);
281 if let Some(ref mut art_idx) = self.art_index
282 && let Some(art_key) = ArtKey::from_value(&row[self.primary_key_column])
283 {
284 art_idx.insert(&art_key, start_offset + i as u64);
285 }
286 }
287 }
288
289 Ok(rows.len() as u64)
290 }
291
292 pub fn lookup_by_pk(&self, pk_value: &Value) -> Option<u64> {
297 let pk_key = pk_value_to_string(pk_value);
298 self.hash_index.lookup(&pk_key)
299 }
300
301 pub fn lookup_by_pk_batch(&self, pk_values: &[Value]) -> Vec<Option<u64>> {
308 pk_values
309 .iter()
310 .map(|pk_value| {
311 let pk_key = pk_value_to_string(pk_value);
312 self.hash_index.lookup(&pk_key)
313 })
314 .collect()
315 }
316
317 pub fn lookup_by_pk_range(
325 &self,
326 lower: Option<&Value>,
327 lower_inclusive: bool,
328 upper: Option<&Value>,
329 upper_inclusive: bool,
330 max_results: u64,
331 ) -> Vec<u64> {
332 match &self.art_index {
333 Some(idx) => {
334 let lower_key = lower.and_then(ArtKey::from_value);
335 let upper_key = upper.and_then(ArtKey::from_value);
336 idx.range_scan(
337 lower_key.as_ref(),
338 lower_inclusive,
339 upper_key.as_ref(),
340 upper_inclusive,
341 max_results,
342 )
343 }
344 None => Vec::new(),
345 }
346 }
347
348 pub fn scan_column(
357 &self,
358 col_idx: usize,
359 start: u64,
360 count: u64,
361 snapshot_ts: Option<u64>,
362 commit_history: &[(u64, u64)],
363 ) -> Vec<Value> {
364 if col_idx >= self.columns.len() || start >= self.num_rows {
365 return Vec::new();
366 }
367 let end = (start + count).min(self.num_rows);
368 let mut result = Vec::with_capacity((end - start) as usize);
369
370 let group_start = self.find_group(start);
372 let mut remaining = end - start;
373
374 for g_idx in group_start..self.node_groups.len() {
375 if remaining == 0 {
376 break;
377 }
378 let group = &self.node_groups[g_idx];
379 let local_start = if g_idx == group_start {
380 (start - group.start_offset) as usize
381 } else {
382 0
383 };
384 let available = (group.num_nodes as usize).saturating_sub(local_start);
385 let take = available.min(remaining as usize);
386
387 for row in local_start..local_start + take {
388 let val = group.get_value_with_snapshot(row, col_idx, snapshot_ts, commit_history);
389 match val {
390 Some(v) => result.push(v.clone()),
391 None => result.push(Value::Null),
392 }
393 }
394 remaining -= take as u64;
395 }
396
397 result
398 }
399
400 pub fn load_persisted_rows(&mut self, rows: Vec<Vec<Value>>) -> Result<(), StorageError> {
408 let num_cols = self.columns.len();
409 self.node_groups.clear();
410 self.hash_index.clear();
411
412 let mut offset = 0u64;
413 let mut group = NodeGroup::new(num_cols, offset);
414 for row in &rows {
415 if row.len() != num_cols {
416 return Err(StorageError::Page(format!(
417 "load_persisted_rows: expected {num_cols} values, got {}",
418 row.len()
419 )));
420 }
421 group.append_row_with_txn(row.clone(), None)?;
422 offset += 1;
423 if group.is_full() {
424 self.node_groups.push(group);
425 group = NodeGroup::new(num_cols, offset);
426 }
427 }
428 if group.num_nodes > 0 {
429 self.node_groups.push(group);
430 }
431 self.num_rows = offset;
432
433 if self.primary_key_column < num_cols {
435 for (row_idx, values) in rows.iter().enumerate() {
436 let pk = &values[self.primary_key_column];
437 if matches!(pk, Value::Null) {
438 continue;
439 }
440 let key = pk_value_to_string(pk);
441 self.hash_index.insert(key, row_idx as u64);
442 }
443 }
444
445 if self.art_index.is_some() && self.primary_key_column < num_cols {
447 if let Some(art) = &mut self.art_index {
448 art.clear();
449 for (row_idx, values) in rows.iter().enumerate() {
450 let pk = &values[self.primary_key_column];
451 if matches!(pk, Value::Null) {
452 continue;
453 }
454 if let Some(key) = ArtKey::from_value(pk) {
455 art.insert(&key, row_idx as u64);
456 }
457 }
458 }
459 }
460 Ok(())
461 }
462
463 pub fn update_cell(&mut self, row_idx: u64, col_idx: usize, value: Value) -> Result<(), StorageError> {
465 if col_idx >= self.columns.len() {
466 return Err(StorageError::Page(format!("Column index {col_idx} out of range")));
467 }
468 if row_idx >= self.num_rows {
469 return Err(StorageError::Page(format!(
470 "Row index {row_idx} out of range (num_rows={})",
471 self.num_rows
472 )));
473 }
474 self.persistence_dirty = true;
475
476 let mut offset = 0u64;
477 for group in &mut self.node_groups {
478 if row_idx < offset + group.num_nodes {
479 let local_row = (row_idx - offset) as usize;
480 if let Some(col_chunk) = group.columns.get_mut(col_idx) {
481 col_chunk.set_value(local_row, value)?;
482 }
483 return Ok(());
484 }
485 offset += group.num_nodes;
486 }
487 Err(StorageError::Page(format!(
488 "Row index {row_idx} not found in any node group"
489 )))
490 }
491
492 pub fn delete_row(&mut self, row_idx: u64) -> Result<(), StorageError> {
495 self.delete_row_with_txn(row_idx, None)
496 }
497
498 pub fn delete_row_with_txn(&mut self, row_idx: u64, txn_id: Option<u64>) -> Result<(), StorageError> {
503 if row_idx >= self.num_rows {
504 return Err(StorageError::Page(format!(
505 "Row index {row_idx} out of range (num_rows={})",
506 self.num_rows
507 )));
508 }
509 self.persistence_dirty = true;
510
511 let mut offset = 0u64;
513 for group in &mut self.node_groups {
514 if row_idx < offset + group.num_nodes {
515 let local_row = (row_idx - offset) as usize;
516 if let Some(txn) = txn_id {
518 if let Some(ref vi) = group.version_info {
519 vi.delete(txn, local_row as u32);
520 }
521 }
522 for col_chunk in &mut group.columns {
524 let _ = col_chunk.set_value(local_row, Value::Null);
525 }
526 return Ok(());
527 }
528 offset += group.num_nodes;
529 }
530 Err(StorageError::Page(format!(
531 "Row index {row_idx} not found in any node group"
532 )))
533 }
534
535 pub fn get_value(&self, row: usize, col: usize) -> Option<&Value> {
538 self.get_value_with_snapshot(row, col, None, &[])
539 }
540
541 pub fn get_value_with_snapshot(
546 &self,
547 row: usize,
548 col: usize,
549 snapshot_ts: Option<u64>,
550 commit_history: &[(u64, u64)],
551 ) -> Option<&Value> {
552 if col >= self.columns.len() || row as u64 >= self.num_rows {
553 return None;
554 }
555 let group_idx = self.find_group(row as u64);
556 let group = self.node_groups.get(group_idx)?;
557 let local_row = row as u64 - group.start_offset;
558 group.get_value_with_snapshot(local_row as usize, col, snapshot_ts, commit_history)
559 }
560
561 pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
565 self.to_column_major_data_with_predicate(None)
566 }
567
568 pub fn to_column_major_data_with_predicate(&self, predicate: Option<(usize, &str, &Value)>) -> Vec<Vec<Value>> {
571 let num_cols = self.columns.len();
572 let mut result = vec![Vec::new(); num_cols]; for group in &self.node_groups {
575 if let Some((col_idx, op, val)) = predicate
576 && let Some(col_chunk) = group.columns.get(col_idx)
577 {
578 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
579 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
580 continue; }
582 }
583
584 for row in 0..group.num_nodes as usize {
585 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
586 match group.get_value(row, col) {
587 Some(v) => res_col.push(v.clone()),
588 None => res_col.push(Value::Null),
589 }
590 }
591 }
592 }
593
594 result
595 }
596
597 pub fn to_column_major_data_with_snapshot(
603 &self,
604 snapshot_ts: Option<u64>,
605 commit_history: &[(u64, u64)],
606 ) -> Vec<Vec<Value>> {
607 let num_cols = self.columns.len();
608 let mut result = vec![Vec::new(); num_cols];
609
610 for group in &self.node_groups {
611 for row in 0..group.num_nodes as usize {
612 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
613 match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
614 Some(v) => res_col.push(v),
615 None => res_col.push(Value::Null),
616 }
617 }
618 }
619 }
620
621 result
622 }
623
624 pub fn to_column_major_data_with_snapshot_and_predicate(
627 &self,
628 predicate: Option<(usize, &str, &Value)>,
629 snapshot_ts: Option<u64>,
630 commit_history: &[(u64, u64)],
631 ) -> Vec<Vec<Value>> {
632 let num_cols = self.columns.len();
633 let mut result = vec![Vec::new(); num_cols];
634
635 for group in &self.node_groups {
636 if let Some((col_idx, op, val)) = predicate
637 && let Some(col_chunk) = group.columns.get(col_idx)
638 {
639 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
640 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
641 continue;
642 }
643 }
644
645 for row in 0..group.num_nodes as usize {
646 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
647 match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
648 Some(v) => res_col.push(v),
649 None => res_col.push(Value::Null),
650 }
651 }
652 }
653 }
654
655 result
656 }
657
658 pub fn to_column_major_data_with_predicate_and_ids(
663 &self,
664 predicate: Option<(usize, &str, &Value)>,
665 ) -> (Vec<Vec<Value>>, Vec<u64>) {
666 let num_cols = self.columns.len();
667 let mut result = vec![Vec::new(); num_cols];
668 let mut ids = Vec::new();
669
670 for group in &self.node_groups {
671 if let Some((col_idx, op, val)) = predicate
672 && let Some(col_chunk) = group.columns.get(col_idx)
673 {
674 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
675 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
676 continue;
677 }
678 }
679
680 for row in 0..group.num_nodes as usize {
681 ids.push(group.start_offset + row as u64);
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, ids)
692 }
693
694 pub fn to_column_major_data_with_snapshot_and_predicate_and_ids(
698 &self,
699 predicate: Option<(usize, &str, &Value)>,
700 snapshot_ts: Option<u64>,
701 commit_history: &[(u64, u64)],
702 ) -> (Vec<Vec<Value>>, Vec<u64>) {
703 let num_cols = self.columns.len();
704 let mut result = vec![Vec::new(); num_cols];
705 let mut ids = Vec::new();
706
707 for group in &self.node_groups {
708 if let Some((col_idx, op, val)) = predicate
709 && let Some(col_chunk) = group.columns.get(col_idx)
710 {
711 use crate::predicate::{ZoneMapCheckResult, check_zone_map};
712 if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
713 continue;
714 }
715 }
716
717 for row in 0..group.num_nodes as usize {
718 ids.push(group.start_offset + row as u64);
719 for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
720 match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
721 Some(v) => res_col.push(v),
722 None => res_col.push(Value::Null),
723 }
724 }
725 }
726 }
727
728 (result, ids)
729 }
730
731 fn find_group(&self, row: u64) -> usize {
733 match self.node_groups.binary_search_by_key(&row, |g| g.start_offset) {
734 Ok(i) => i,
735 Err(i) => {
736 if i == 0 {
737 0
738 } else {
739 i - 1
740 }
741 }
742 }
743 }
744}
745
746fn pk_value_to_string(v: &Value) -> String {
748 match v {
749 Value::Null => "null".to_string(),
750 Value::Bool(b) => b.to_string(),
751 Value::Int64(i) => i.to_string(),
752 Value::Int32(i) => i.to_string(),
753 Value::Int16(i) => i.to_string(),
754 Value::Int8(i) => i.to_string(),
755 Value::UInt64(u) => u.to_string(),
756 Value::UInt32(u) => u.to_string(),
757 Value::UInt16(u) => u.to_string(),
758 Value::UInt8(u) => u.to_string(),
759 Value::Double(f) => f.to_string(),
760 Value::Float(f) => f.to_string(),
761 Value::String(s) => s.clone(),
762 Value::Date(d) => format!("Date({})", d.0),
763 Value::Timestamp(ts) => format!("Timestamp({})", ts.0),
764 other => format!("{other:?}"),
765 }
766}
767
768fn coerce_values_to_columns(values: &mut [Value], columns: &[ColumnDefinition]) -> Result<(), StorageError> {
776 for (i, col) in columns.iter().enumerate() {
777 let coerced = match (col.logical_type, &values[i]) {
778 (LogicalTypeID::UInt64, Value::Int64(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
779 (LogicalTypeID::UInt64, Value::Int32(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
780 (LogicalTypeID::UInt64, Value::Int16(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
781 (LogicalTypeID::UInt64, Value::Int8(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
782 (LogicalTypeID::UInt64, Value::UInt32(x)) => Some(Value::UInt64(*x as u64)),
783 (LogicalTypeID::UInt64, Value::UInt16(x)) => Some(Value::UInt64(*x as u64)),
784 (LogicalTypeID::UInt64, Value::UInt8(x)) => Some(Value::UInt64(*x as u64)),
785 (LogicalTypeID::UInt64, Value::Int64(_))
786 | (LogicalTypeID::UInt64, Value::Int32(_))
787 | (LogicalTypeID::UInt64, Value::Int16(_))
788 | (LogicalTypeID::UInt64, Value::Int8(_)) => {
789 return Err(StorageError::Page(format!(
790 "Cannot store negative value in UINT64 column '{}'",
791 col.name
792 )));
793 }
794 _ => None,
795 };
796 if let Some(c) = coerced {
797 values[i] = c;
798 }
799 }
800 Ok(())
801}
802
803#[derive(Debug, Clone)]
815pub struct RelTable {
816 pub table_id: u64,
817 pub name: String,
818 pub src_table_id: u64,
819 pub dst_table_id: u64,
820 pub columns: Vec<ColumnDefinition>,
821 pub num_rows: u64,
822 pub edges: Vec<(u64, u64)>,
824 pub fwd_adj: HashMap<u64, Vec<(u64, usize)>>,
826 pub rev_adj: HashMap<u64, Vec<(u64, usize)>>,
828 pub csr_index: Option<CsrIndex>,
830 pub properties: Vec<Vec<Value>>,
832 pub persistence_dirty: bool,
835}
836
837impl RelTable {
838 pub fn new(
839 table_id: u64,
840 name: String,
841 src_table_id: u64,
842 dst_table_id: u64,
843 columns: Vec<ColumnDefinition>,
844 ) -> Self {
845 let num_cols = columns.len();
846 Self {
847 table_id,
848 name,
849 src_table_id,
850 dst_table_id,
851 columns,
852 num_rows: 0,
853 edges: Vec::new(),
854 fwd_adj: HashMap::new(),
855 rev_adj: HashMap::new(),
856 csr_index: None,
857 properties: vec![Vec::new(); num_cols],
858 persistence_dirty: false,
859 }
860 }
861
862 pub fn insert_rel(&mut self, from: u64, to: u64, values: Vec<Value>) -> Result<(), StorageError> {
870 if values.len() != self.columns.len() {
871 return Err(StorageError::Page(format!(
872 "Column count mismatch: expected {} values, got {}",
873 self.columns.len(),
874 values.len()
875 )));
876 }
877
878 let edge_idx = self.edges.len();
879 self.edges.push((from, to));
880
881 self.fwd_adj.entry(from).or_default().push((to, edge_idx));
883
884 self.rev_adj.entry(to).or_default().push((from, edge_idx));
886 for (col_idx, val) in values.into_iter().enumerate() {
888 self.properties[col_idx].push(val);
889 }
890 self.num_rows += 1;
891 Ok(())
892 }
893
894 pub fn insert_rels_batch(&mut self, rels: &[(u64, u64, Vec<Value>)]) -> Result<u64, StorageError> {
897 if rels.is_empty() {
898 return Ok(0);
899 }
900 let num_cols = self.columns.len();
901 let total = rels.len();
902
903 for (i, (_, _, vals)) in rels.iter().enumerate() {
905 if vals.len() != num_cols {
906 return Err(StorageError::Page(format!(
907 "Rel {} column count mismatch: expected {} values, got {}",
908 i,
909 num_cols,
910 vals.len()
911 )));
912 }
913 }
914
915 self.edges.reserve(total);
917 for col in &mut self.properties {
918 col.reserve(total);
919 }
920
921 let _start_edge_idx = self.edges.len();
922
923 for (from, to, vals) in rels {
925 let edge_idx = self.edges.len();
926 self.edges.push((*from, *to));
927 self.fwd_adj.entry(*from).or_default().push((*to, edge_idx));
928 self.rev_adj.entry(*to).or_default().push((*from, edge_idx));
929 for (col_idx, val) in vals.iter().enumerate() {
930 self.properties[col_idx].push(val.clone());
931 }
932 }
933
934 self.num_rows += total as u64;
935 Ok(total as u64)
936 }
937
938 pub fn delete_edge(&mut self, edge_idx: usize) -> Result<(), StorageError> {
941 if edge_idx >= self.edges.len() {
942 return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
943 }
944
945 let (src, dst) = self.edges[edge_idx];
946 if src == u64::MAX {
947 return Ok(());
949 }
950
951 if let Some(adj) = self.fwd_adj.get_mut(&src) {
953 adj.retain(|&(_, idx)| idx != edge_idx);
954 }
955
956 if let Some(adj) = self.rev_adj.get_mut(&dst) {
958 adj.retain(|&(_, idx)| idx != edge_idx);
959 }
960
961 self.edges[edge_idx] = (u64::MAX, u64::MAX);
963 self.persistence_dirty = true;
964
965 for col in &mut self.properties {
967 if edge_idx < col.len() {
968 col[edge_idx] = Value::Null;
969 }
970 }
971
972 Ok(())
973 }
974
975 pub fn update_cell(&mut self, edge_idx: usize, col_idx: usize, value: Value) -> Result<(), StorageError> {
977 if col_idx >= self.columns.len() {
978 return Err(StorageError::Page(format!("Column index {col_idx} out of range")));
979 }
980 if edge_idx >= self.properties[col_idx].len() {
981 return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
982 }
983
984 self.properties[col_idx][edge_idx] = value;
985 self.persistence_dirty = true;
986 Ok(())
987 }
988
989 pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
993 let num_prop_cols = self.columns.len();
997 if values.len() != num_prop_cols {
998 return Err(StorageError::Page(format!(
999 "Column count mismatch: expected {} values, got {}",
1000 num_prop_cols,
1001 values.len()
1002 )));
1003 }
1004
1005 let from = self.num_rows;
1008 let to = self.num_rows;
1009 self.insert_rel(from, to, values)?;
1010 Ok(0) }
1012
1013 pub fn scan_adj_list(&self, src_offset: u64) -> &[(u64, usize)] {
1018 self.fwd_adj.get(&src_offset).map(|v| v.as_slice()).unwrap_or(&[])
1019 }
1020
1021 pub fn scan_rev_adj_list(&self, dst_offset: u64) -> &[(u64, usize)] {
1026 self.rev_adj.get(&dst_offset).map(|v| v.as_slice()).unwrap_or(&[])
1027 }
1028
1029 pub fn get_outgoing_edges(&self, src_offset: u64) -> Vec<(u64, Vec<Value>)> {
1031 self.scan_adj_list(src_offset)
1032 .iter()
1033 .map(|&(dst, edge_idx)| {
1034 let props = self.get_edge_properties(edge_idx);
1035 (dst, props)
1036 })
1037 .collect()
1038 }
1039
1040 pub fn get_incoming_edges(&self, dst_offset: u64) -> Vec<(u64, Vec<Value>)> {
1042 self.scan_rev_adj_list(dst_offset)
1043 .iter()
1044 .map(|&(src, edge_idx)| {
1045 let props = self.get_edge_properties(edge_idx);
1046 (src, props)
1047 })
1048 .collect()
1049 }
1050
1051 pub fn get_edge_properties(&self, edge_idx: usize) -> Vec<Value> {
1053 let mut props = Vec::with_capacity(self.columns.len());
1054 for col in &self.properties {
1055 match col.get(edge_idx) {
1056 Some(v) => props.push(v.clone()),
1057 None => props.push(Value::Null),
1058 }
1059 }
1060 props
1061 }
1062
1063 pub fn get_column(&self, col_idx: usize) -> Option<&[Value]> {
1065 self.properties.get(col_idx).map(|v| v.as_slice())
1066 }
1067
1068 pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
1070 self.properties.clone()
1071 }
1072}
1073
1074#[derive(Debug, Default)]
1081pub struct TableCatalog {
1082 node_tables: DashMap<u64, NodeTable>,
1083 rel_tables: DashMap<u64, RelTable>,
1084 vector_indexes: DashMap<u64, VectorIndexTable>,
1085 node_name_to_id: DashMap<String, u64>,
1087 rel_name_to_id: DashMap<String, u64>,
1089 vector_index_name_to_id: DashMap<String, u64>,
1091 next_table_id: std::sync::atomic::AtomicU64,
1092}
1093
1094impl TableCatalog {
1095 pub fn new() -> Self {
1096 Self::default()
1097 }
1098
1099 pub fn create_node_table(&self, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1100 let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1101 let table = NodeTable::new(table_id, name.clone(), columns);
1102 self.node_name_to_id.insert(name, table_id);
1103 self.node_tables.insert(table_id, table.clone());
1104 table
1105 }
1106
1107 pub fn create_node_table_with_id(&self, table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1111 self.bump_next_table_id(table_id);
1112 let table = NodeTable::new(table_id, name.clone(), columns);
1113 self.node_name_to_id.insert(name, table_id);
1114 self.node_tables.insert(table_id, table.clone());
1115 table
1116 }
1117
1118 pub fn create_rel_table(
1119 &self,
1120 name: String,
1121 src_table_id: u64,
1122 dst_table_id: u64,
1123 columns: Vec<ColumnDefinition>,
1124 ) -> RelTable {
1125 let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1126 let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1127 self.rel_name_to_id.insert(name, table_id);
1128 self.rel_tables.insert(table_id, table.clone());
1129 table
1130 }
1131
1132 pub fn create_rel_table_with_id(
1136 &self,
1137 table_id: u64,
1138 name: String,
1139 src_table_id: u64,
1140 dst_table_id: u64,
1141 columns: Vec<ColumnDefinition>,
1142 ) -> RelTable {
1143 self.bump_next_table_id(table_id);
1144 let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1145 self.rel_name_to_id.insert(name, table_id);
1146 self.rel_tables.insert(table_id, table.clone());
1147 table
1148 }
1149
1150 fn bump_next_table_id(&self, table_id: u64) {
1153 let mut next = self.next_table_id.load(std::sync::atomic::Ordering::SeqCst);
1154 while next <= table_id {
1155 match self.next_table_id.compare_exchange(
1156 next,
1157 table_id + 1,
1158 std::sync::atomic::Ordering::SeqCst,
1159 std::sync::atomic::Ordering::SeqCst,
1160 ) {
1161 Ok(_) => break,
1162 Err(current) => next = current,
1163 }
1164 }
1165 }
1166
1167 pub fn get_node_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1168 self.node_tables.get(&table_id)
1169 }
1170
1171 pub fn get_node_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1172 self.node_tables.get_mut(&table_id)
1173 }
1174
1175 pub fn get_node_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1176 let id = self.node_name_to_id.get(name)?;
1177 self.node_tables.get(&*id)
1178 }
1179
1180 pub fn get_node_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1181 let id = self.node_name_to_id.get(name)?;
1182 self.node_tables.get_mut(&*id)
1183 }
1184
1185 pub fn get_rel_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1186 self.rel_tables.get(&table_id)
1187 }
1188
1189 pub fn get_rel_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1190 self.rel_tables.get_mut(&table_id)
1191 }
1192
1193 pub fn get_rel_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1194 let id = self.rel_name_to_id.get(name)?;
1195 self.rel_tables.get(&*id)
1196 }
1197
1198 pub fn get_rel_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1199 let id = self.rel_name_to_id.get(name)?;
1200 self.rel_tables.get_mut(&*id)
1201 }
1202
1203 pub fn has_incident_edges(&self, table_id: u64, node_idx: u64) -> bool {
1205 for rel_table in self.rel_tables.iter() {
1206 if rel_table.src_table_id == table_id {
1207 if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1208 if !edges.is_empty() {
1209 return true;
1210 }
1211 }
1212 }
1213 if rel_table.dst_table_id == table_id {
1214 if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1215 if !edges.is_empty() {
1216 return true;
1217 }
1218 }
1219 }
1220 }
1221 false
1222 }
1223
1224 pub fn detach_node(&self, table_id: u64, node_idx: u64) {
1226 for mut rel_table in self.rel_tables.iter_mut() {
1227 let mut edges_to_delete = Vec::new();
1228
1229 if rel_table.src_table_id == table_id {
1230 if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1231 for &(_, edge_idx) in edges {
1232 edges_to_delete.push(edge_idx);
1233 }
1234 }
1235 }
1236
1237 if rel_table.dst_table_id == table_id {
1238 if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1239 for &(_, edge_idx) in edges {
1240 edges_to_delete.push(edge_idx);
1241 }
1242 }
1243 }
1244
1245 for edge_idx in edges_to_delete {
1246 let _ = rel_table.delete_edge(edge_idx);
1247 }
1248 }
1249 }
1250
1251 pub fn all_node_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, NodeTable>> {
1252 self.node_tables.iter().collect()
1253 }
1254
1255 pub fn all_rel_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, RelTable>> {
1256 self.rel_tables.iter().collect()
1257 }
1258
1259 pub fn node_table_num_rows(&self, name: &str) -> u64 {
1261 self.get_node_table_by_name(name).map(|t| t.num_rows).unwrap_or(0)
1262 }
1263
1264 pub fn drop_node_table(&self, name: &str) -> bool {
1266 if let Some(id) = self.node_name_to_id.get(name) {
1267 let table_id = *id;
1268 drop(id);
1269 self.node_name_to_id.remove(name);
1270 self.node_tables.remove(&table_id).is_some()
1271 } else {
1272 false
1273 }
1274 }
1275
1276 pub fn create_vector_index(
1278 &self,
1279 name: String,
1280 table_name: String,
1281 column_name: String,
1282 metric: DistanceMetric,
1283 dimensions: u32,
1284 ) -> VectorIndexTable {
1285 let index_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1286 let table = VectorIndexTable::new(index_id, name.clone(), table_name, column_name, metric, dimensions);
1287 self.vector_index_name_to_id.insert(name, index_id);
1288 self.vector_indexes.insert(index_id, table.clone());
1289 table
1290 }
1291
1292 pub fn get_vector_index(&self, index_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1294 self.vector_indexes.get(&index_id)
1295 }
1296
1297 pub fn get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1299 let id = self.vector_index_name_to_id.get(name)?;
1300 self.vector_indexes.get(&*id)
1301 }
1302
1303 pub fn get_vector_index_by_name_mut(
1305 &self,
1306 name: &str,
1307 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1308 let id = self.vector_index_name_to_id.get(name)?;
1309 self.vector_indexes.get_mut(&*id)
1310 }
1311
1312 pub fn get_vector_index_mut(
1314 &self,
1315 index_id: u64,
1316 ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1317 self.vector_indexes.get_mut(&index_id)
1318 }
1319
1320 pub fn drop_vector_index(&self, name: &str) -> bool {
1322 if let Some(id) = self.vector_index_name_to_id.get(name) {
1323 let index_id = *id;
1324 drop(id);
1325 self.vector_index_name_to_id.remove(name);
1326 self.vector_indexes.remove(&index_id).is_some()
1327 } else {
1328 false
1329 }
1330 }
1331
1332 pub fn all_vector_indexes(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, VectorIndexTable>> {
1334 self.vector_indexes.iter().collect()
1335 }
1336
1337 pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
1344 let mut table = self
1345 .get_node_table_by_name_mut(table_name)
1346 .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1347
1348 if table.art_index.is_some() {
1349 return Err(StorageError::Index(format!(
1350 "Table '{table_name}' already has an ART index"
1351 )));
1352 }
1353
1354 let mut art_idx = ArtPrimaryKeyIndex::new(index_name);
1355
1356 let pk_col = table.primary_key_column;
1358 let col_major = table.to_column_major_data();
1360 if pk_col < col_major.len() {
1361 for (row_offset, pk_val) in col_major[pk_col].iter().enumerate() {
1362 if !matches!(pk_val, Value::Null)
1363 && let Some(art_key) = ArtKey::from_value(pk_val)
1364 {
1365 art_idx.insert(&art_key, row_offset as u64);
1366 }
1367 }
1368 }
1369
1370 table.art_index = Some(art_idx);
1371 Ok(())
1372 }
1373
1374 pub fn drop_art_index(&self, table_name: &str) -> Result<(), StorageError> {
1376 let mut table = self
1377 .get_node_table_by_name_mut(table_name)
1378 .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1379
1380 table.art_index = None;
1381 Ok(())
1382 }
1383
1384 pub fn get_art_index(&self, table_name: &str) -> Option<ArtPrimaryKeyIndex> {
1387 let table = self.get_node_table_by_name(table_name)?;
1388 table.art_index.clone()
1389 }
1390
1391 pub fn has_art_index(&self, table_name: &str) -> bool {
1393 self.get_node_table_by_name(table_name)
1394 .map(|t| t.art_index.is_some())
1395 .unwrap_or(false)
1396 }
1397
1398 pub fn drop_rel_table(&self, name: &str) -> bool {
1400 if let Some(id) = self.rel_name_to_id.get(name) {
1401 let table_id = *id;
1402 drop(id);
1403 self.rel_name_to_id.remove(name);
1404 self.rel_tables.remove(&table_id).is_some()
1405 } else {
1406 false
1407 }
1408 }
1409}
1410
1411#[cfg(test)]
1416mod tests {
1417 use super::*;
1418 use crate::column_chunk::NODE_GROUP_SIZE;
1419
1420 #[test]
1423 fn test_node_table_empty() {
1424 let table = NodeTable::new(
1425 1,
1426 "Person".into(),
1427 vec![
1428 ColumnDefinition {
1429 compression: akar_common::enums::CompressionType::Uncompressed,
1430 name: "name".into(),
1431 logical_type: LogicalTypeID::String,
1432 is_primary_key: true,
1433 },
1434 ColumnDefinition {
1435 compression: akar_common::enums::CompressionType::Uncompressed,
1436 name: "age".into(),
1437 logical_type: LogicalTypeID::Int64,
1438 is_primary_key: false,
1439 },
1440 ],
1441 );
1442 assert_eq!(table.num_rows, 0);
1443 assert!(table.node_groups.is_empty());
1444 }
1445
1446 #[test]
1447 fn test_node_table_insert_and_get() {
1448 let mut table = NodeTable::new(
1449 1,
1450 "Person".into(),
1451 vec![
1452 ColumnDefinition {
1453 compression: akar_common::enums::CompressionType::Uncompressed,
1454 name: "name".into(),
1455 logical_type: LogicalTypeID::String,
1456 is_primary_key: true,
1457 },
1458 ColumnDefinition {
1459 compression: akar_common::enums::CompressionType::Uncompressed,
1460 name: "age".into(),
1461 logical_type: LogicalTypeID::Int64,
1462 is_primary_key: false,
1463 },
1464 ],
1465 );
1466 table
1467 .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1468 .unwrap();
1469 table
1470 .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1471 .unwrap();
1472
1473 assert_eq!(table.num_rows, 2);
1474 assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1475 assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1476 }
1477
1478 #[test]
1479 fn test_node_table_scan_column() {
1480 let mut table = NodeTable::new(
1481 1,
1482 "T".into(),
1483 vec![ColumnDefinition {
1484 compression: akar_common::enums::CompressionType::Uncompressed,
1485 name: "val".into(),
1486 logical_type: LogicalTypeID::Int64,
1487 is_primary_key: false,
1488 }],
1489 );
1490 for i in 0..100 {
1491 table.insert_row(vec![Value::Int64(i)]).unwrap();
1492 }
1493 let scanned = table.scan_column(0, 10, 5, None, &[]);
1494 assert_eq!(scanned.len(), 5);
1495 assert_eq!(scanned[0], Value::Int64(10));
1496 assert_eq!(scanned[4], Value::Int64(14));
1497 }
1498
1499 #[test]
1500 fn test_node_table_to_column_major() {
1501 let mut table = NodeTable::new(
1502 1,
1503 "T".into(),
1504 vec![
1505 ColumnDefinition {
1506 compression: akar_common::enums::CompressionType::Uncompressed,
1507 name: "x".into(),
1508 logical_type: LogicalTypeID::Int64,
1509 is_primary_key: false,
1510 },
1511 ColumnDefinition {
1512 compression: akar_common::enums::CompressionType::Uncompressed,
1513 name: "y".into(),
1514 logical_type: LogicalTypeID::Int64,
1515 is_primary_key: false,
1516 },
1517 ],
1518 );
1519 table.insert_row(vec![Value::Int64(1), Value::Int64(10)]).unwrap();
1520 table.insert_row(vec![Value::Int64(2), Value::Int64(20)]).unwrap();
1521
1522 let data = table.to_column_major_data();
1523 assert_eq!(data.len(), 2);
1524 assert_eq!(data[0], vec![Value::Int64(1), Value::Int64(2)]);
1525 assert_eq!(data[1], vec![Value::Int64(10), Value::Int64(20)]);
1526 }
1527
1528 #[test]
1529 fn test_node_table_auto_node_group() {
1530 let mut table = NodeTable::new(
1531 1,
1532 "T".into(),
1533 vec![ColumnDefinition {
1534 compression: akar_common::enums::CompressionType::Uncompressed,
1535 name: "v".into(),
1536 logical_type: LogicalTypeID::Int64,
1537 is_primary_key: false,
1538 }],
1539 );
1540 for i in 0..NODE_GROUP_SIZE as u64 + 1 {
1542 table.insert_row(vec![Value::Int64(i as i64)]).unwrap();
1543 }
1544 assert_eq!(table.num_rows, NODE_GROUP_SIZE as u64 + 1);
1545 assert_eq!(table.node_groups.len(), 2);
1546 assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1547 assert_eq!(table.node_groups[1].num_nodes, 1);
1548 assert_eq!(table.get_value(0, 0), Some(&Value::Int64(0)));
1550 assert_eq!(
1551 table.get_value(NODE_GROUP_SIZE, 0),
1552 Some(&Value::Int64(NODE_GROUP_SIZE as i64))
1553 );
1554 }
1555
1556 fn make_rel_table() -> RelTable {
1559 RelTable::new(
1560 1,
1561 "Knows".into(),
1562 0,
1563 1,
1564 vec![
1565 ColumnDefinition {
1566 compression: akar_common::enums::CompressionType::Uncompressed,
1567 name: "since".into(),
1568 logical_type: LogicalTypeID::Int64,
1569 is_primary_key: false,
1570 },
1571 ColumnDefinition {
1572 compression: akar_common::enums::CompressionType::Uncompressed,
1573 name: "weight".into(),
1574 logical_type: LogicalTypeID::Double,
1575 is_primary_key: false,
1576 },
1577 ],
1578 )
1579 }
1580
1581 #[test]
1582 fn test_rel_table_empty() {
1583 let rel = make_rel_table();
1584 assert_eq!(rel.num_rows, 0);
1585 assert!(rel.edges.is_empty());
1586 assert!(rel.fwd_adj.is_empty());
1587 assert!(rel.rev_adj.is_empty());
1588 }
1589
1590 #[test]
1591 fn test_rel_insert_basic() {
1592 let mut rel = make_rel_table();
1593 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1594 .unwrap();
1595 rel.insert_rel(0, 2, vec![Value::Int64(2021), Value::Double(0.8)])
1596 .unwrap();
1597 rel.insert_rel(1, 0, vec![Value::Int64(2020), Value::Double(0.3)])
1598 .unwrap();
1599
1600 assert_eq!(rel.num_rows, 3);
1601 assert_eq!(rel.edges.len(), 3);
1602
1603 let fwd = rel.scan_adj_list(0);
1605 assert_eq!(fwd.len(), 2);
1606 assert_eq!(fwd[0], (1, 0)); assert_eq!(fwd[1], (2, 1)); let fwd1 = rel.scan_adj_list(1);
1611 assert_eq!(fwd1.len(), 1);
1612 assert_eq!(fwd1[0], (0, 2));
1613 }
1614
1615 #[test]
1616 fn test_rel_reverse_adjacency() {
1617 let mut rel = make_rel_table();
1618 rel.insert_rel(0, 5, vec![Value::Int64(2022), Value::Double(1.0)])
1619 .unwrap();
1620 rel.insert_rel(3, 5, vec![Value::Int64(2023), Value::Double(1.5)])
1621 .unwrap();
1622
1623 let rev = rel.scan_rev_adj_list(5);
1625 assert_eq!(rev.len(), 2);
1626 assert_eq!(rev[0], (0, 0));
1627 assert_eq!(rev[1], (3, 1));
1628 }
1629
1630 #[test]
1631 fn test_rel_get_edge_properties() {
1632 let mut rel = make_rel_table();
1633 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1634 .unwrap();
1635 rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
1636 .unwrap();
1637
1638 let props0 = rel.get_edge_properties(0);
1639 assert_eq!(props0, vec![Value::Int64(2020), Value::Double(0.5)]);
1640
1641 let props1 = rel.get_edge_properties(1);
1642 assert_eq!(props1, vec![Value::Int64(2021), Value::Double(0.9)]);
1643 }
1644
1645 #[test]
1646 fn test_rel_get_outgoing_edges() {
1647 let mut rel = make_rel_table();
1648 rel.insert_rel(0, 10, vec![Value::Int64(2020), Value::Double(1.0)])
1649 .unwrap();
1650 rel.insert_rel(0, 20, vec![Value::Int64(2021), Value::Double(2.0)])
1651 .unwrap();
1652
1653 let outgoing = rel.get_outgoing_edges(0);
1654 assert_eq!(outgoing.len(), 2);
1655 assert_eq!(outgoing[0].0, 10);
1656 assert_eq!(outgoing[0].1, vec![Value::Int64(2020), Value::Double(1.0)]);
1657 assert_eq!(outgoing[1].0, 20);
1658 }
1659
1660 #[test]
1661 fn test_rel_get_incoming_edges() {
1662 let mut rel = make_rel_table();
1663 rel.insert_rel(10, 5, vec![Value::Int64(2020), Value::Double(1.0)])
1664 .unwrap();
1665 rel.insert_rel(20, 5, vec![Value::Int64(2021), Value::Double(2.0)])
1666 .unwrap();
1667
1668 let incoming = rel.get_incoming_edges(5);
1669 assert_eq!(incoming.len(), 2);
1670 assert_eq!(incoming[0].0, 10);
1671 assert_eq!(incoming[1].0, 20);
1672 }
1673
1674 #[test]
1675 fn test_rel_no_edges() {
1676 let rel = make_rel_table();
1677 assert!(rel.scan_adj_list(0).is_empty());
1678 assert!(rel.scan_rev_adj_list(0).is_empty());
1679 assert!(rel.get_outgoing_edges(0).is_empty());
1680 assert!(rel.get_incoming_edges(0).is_empty());
1681 }
1682
1683 #[test]
1684 fn test_rel_insert_row_legacy() {
1685 let mut rel = make_rel_table();
1686 rel.insert_row(vec![Value::Int64(2022), Value::Double(3.0)]).unwrap();
1688 assert_eq!(rel.num_rows, 1);
1689 assert_eq!(rel.edges[0], (0, 0)); assert_eq!(rel.get_edge_properties(0), vec![Value::Int64(2022), Value::Double(3.0)]);
1691 }
1692
1693 #[test]
1694 fn test_rel_wrong_column_count() {
1695 let mut rel = make_rel_table();
1696 let result = rel.insert_rel(0, 1, vec![Value::Int64(42)]); assert!(result.is_err());
1698 }
1699
1700 #[test]
1701 fn test_rel_get_column() {
1702 let mut rel = make_rel_table();
1703 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(1.5)])
1704 .unwrap();
1705 rel.insert_rel(1, 2, vec![Value::Int64(2021), Value::Double(2.5)])
1706 .unwrap();
1707
1708 let since_col = rel.get_column(0).unwrap();
1709 assert_eq!(since_col, &[Value::Int64(2020), Value::Int64(2021)]);
1710
1711 let weight_col = rel.get_column(1).unwrap();
1712 assert_eq!(weight_col, &[Value::Double(1.5), Value::Double(2.5)]);
1713 }
1714
1715 #[test]
1716 fn test_rel_to_column_major() {
1717 let mut rel = make_rel_table();
1718 rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1719 .unwrap();
1720 rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
1721 .unwrap();
1722
1723 let data = rel.to_column_major_data();
1724 assert_eq!(data.len(), 2);
1725 assert_eq!(data[0], vec![Value::Int64(2020), Value::Int64(2021)]);
1726 assert_eq!(data[1], vec![Value::Double(0.5), Value::Double(0.9)]);
1727 }
1728
1729 #[test]
1732 fn test_catalog_create_and_lookup() {
1733 let cat = TableCatalog::new();
1734 let node_table = cat.create_node_table(
1735 "Person".into(),
1736 vec![ColumnDefinition {
1737 compression: akar_common::enums::CompressionType::Uncompressed,
1738 name: "id".into(),
1739 logical_type: LogicalTypeID::Int64,
1740 is_primary_key: true,
1741 }],
1742 );
1743 assert_eq!(node_table.table_id, 0);
1744
1745 let rel_table = cat.create_rel_table(
1746 "Knows".into(),
1747 0,
1748 1,
1749 vec![ColumnDefinition {
1750 compression: akar_common::enums::CompressionType::Uncompressed,
1751 name: "since".into(),
1752 logical_type: LogicalTypeID::Int64,
1753 is_primary_key: false,
1754 }],
1755 );
1756 assert_eq!(rel_table.table_id, 1);
1757
1758 assert!(cat.get_node_table(0).is_some());
1759 assert!(cat.get_rel_table(1).is_some());
1760 assert_eq!(cat.node_table_num_rows("Person"), 0);
1761 }
1762}