Skip to main content

akar_storage/
table.rs

1//! Table storage — columnar node/rel tables with NodeGroup-based storage.
2
3use 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/// A column definition within a table.
16#[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/// A node table stores properties for a node label using NodeGroup-based
25/// columnar storage. Data is held in-memory as `NodeGroup`s; when a group
26/// reaches `NODE_GROUP_SIZE` rows a new group is automatically created.
27///
28/// Primary key uniqueness is enforced via an in-memory `HashIndex` that maps
29/// PK values to row offsets. For persistent indexes, use `OnDiskHashIndex`
30/// alongside the L1 cache (see `index.rs`).
31///
32/// Optionally, an `ArtPrimaryKeyIndex` can be attached for range-scan
33/// support on the primary key column (see `create_art_index`).
34#[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    /// NodeGroup-based columnar storage. Each group holds up to
42    /// `NODE_GROUP_SIZE` rows across all columns.
43    pub node_groups: Vec<NodeGroup>,
44    /// In-memory hash index for primary key → row lookup and dedup.
45    /// Stores the PK value (as a string representation) mapped to row offset.
46    pub hash_index: HashIndex<String>,
47    /// Optional ART (Adaptive Radix Tree) index for PK range scans.
48    /// When present, `insert_row()` also updates this index automatically.
49    pub art_index: Option<ArtPrimaryKeyIndex>,
50    /// Set when an UPDATE/DELETE touches the table. The durable column mirror
51    /// (see `persistence.rs`) performs a full rewrite when this flag is set.
52    pub persistence_dirty: bool,
53}
54
55/// Sentinel for `NodeTable::primary_key_column` when a node table has no PK
56/// column. SQL always requires a PK (binder rejects `CREATE NODE TABLE` without
57/// one), so via the SQL path this is unreachable; the sentinel only covers
58/// internally/test-constructed tables and keeps "no PK" explicit instead of
59/// silently defaulting to column 0 (P49.2).
60pub const NO_PRIMARY_KEY: usize = usize::MAX;
61
62impl NodeTable {
63    pub fn new(table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> Self {
64        // SQL guarantees a PK exists (binder/mod.rs:861 rejects tables without
65        // one), so the fallback is unreachable in production. Use an explicit
66        // sentinel instead of a silent 0 so a table with no PK column never
67        // accidentally dedups on column 0 (P49.2).
68        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    /// Insert a row of values into the table.
83    ///
84    /// Appends to the current `NodeGroup`; auto-creates a new group when the
85    /// current one is full (reaches `NODE_GROUP_SIZE` rows).
86    ///
87    /// If the table has a primary key column, checks for duplicates and rejects
88    /// rows with already-existing PK values. The hash index is updated after
89    /// a successful insert.
90    ///
91    /// When `txn_id` is `Some(...)`, the insert is recorded in VersionInfo
92    /// for MVCC snapshot isolation.
93    ///
94    /// Returns an error if the number of values doesn't match the number of columns,
95    /// or if a duplicate primary key value is detected.
96    pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
97        self.insert_row_with_txn(values, None)
98    }
99
100    /// Insert a row with an optional transaction ID for MVCC tracking.
101    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        // Reject NULL primary key values
111        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 literal values to the declared column types (P48.12): constant
122        // evaluation (e.g. CREATE `{id: 41}`) produces Int64 literals regardless
123        // of the target column, and a UINT64 column must store Value::UInt64 so
124        // the scan builds the correct Arrow type instead of dropping the value.
125        coerce_values_to_columns(&mut values, &self.columns)?;
126
127        // Check primary key uniqueness
128        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        // Get or create the current node group.
140        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            // Enable version info if MVCC tracking is requested
145            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        // Enable version info on existing group if needed
153        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        // Update hash index with the PK value for this row
160        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            // Also update ART index if present
166            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    /// Batch insert multiple rows efficiently.
177    /// Validates PK uniqueness, pre-allocates node groups, and bulk-appends.
178    /// When `txn_id` is `Some(...)`, inserts are recorded in VersionInfo for MVCC.
179    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    /// Batch insert with optional MVCC tracking.
184    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        // Validate all rows first
195        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            // Reject NULL primary key values
205            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            // Check PK uniqueness
215            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        // Coerce literal values to the declared column types (P48.12), mirroring
230        // `insert_row_with_txn`. This must happen before appending and index
231        // updates so PK hash/ART keys use the coerced (UInt64) encoding.
232        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        // Ensure we have a node group with enough capacity
239        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        // Append to the last group (spilling into new groups if needed)
253        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        // Batch update indexes
277        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    /// Look up a row offset by its primary key value.
293    ///
294    /// Returns `Some(row_offset)` if the PK exists, or `None` if not found.
295    /// Uses the in-memory hash index for O(1) lookup.
296    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    /// Batch look up row offsets for multiple primary key values.
302    ///
303    /// Returns a `Vec<Option<u64>>` parallel to the input, where each element
304    /// is `Some(row_offset)` if the PK exists, or `None` if not found.
305    /// Uses the in-memory hash index for O(1) per-key lookup, avoiding
306    /// per-row method-call overhead by inlining the lookup logic.
307    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    /// Perform a range scan on the primary key column using the ART index.
318    ///
319    /// Returns up to `max_results` row offsets for keys within `[lower, upper]`
320    /// (respecting inclusivity flags). Returns an empty vec if no ART index
321    /// exists or no keys match.
322    ///
323    /// This is the bridge function called by `PhysicalArtIndexRangeScan`.
324    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    /// Scan all values for a given column across all node groups.
349    ///
350    /// Returns a flat `Vec<Value>` containing values from `start` to
351    /// `start + count` (or fewer if the end of the table is reached).
352    ///
353    /// If `snapshot_ts` is `Some(...)`, performs MVCC snapshot isolation:
354    /// rows inserted/deleted by transactions committed after `snapshot_ts`
355    /// are excluded, and versioned updates are resolved.
356    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        // Find the first node group containing `start`.
371        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    /// Rebuild the table's in-memory state from rows loaded off the durable
401    /// column mirror (see `persistence.rs`).
402    ///
403    /// Populates `node_groups`, `num_rows`, the PK hash index, and the ART
404    /// index directly, bypassing PK uniqueness checks so that soft-deleted
405    /// rows (whose PK is `Null`) can be restored at their original row
406    /// offsets.
407    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        // Rebuild the PK hash index (skip soft-deleted rows with a Null PK).
434        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        // Rebuild the ART index if present.
446        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    /// Update a single cell (row, column) with a new value.
464    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    /// Delete a row by its index. Marks the row as null by setting all its column
493    /// values to `Value::Null`. This is a soft delete — the row slot remains.
494    pub fn delete_row(&mut self, row_idx: u64) -> Result<(), StorageError> {
495        self.delete_row_with_txn(row_idx, None)
496    }
497
498    /// Delete a row with optional MVCC tracking.
499    ///
500    /// When `txn_id` is `Some(...)`, the delete is recorded in VersionInfo
501    /// for MVCC snapshot isolation.
502    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        // Locate the node group containing this row
512        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                // Record delete in VersionInfo if MVCC tracking is active
517                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                // Set all columns to Null for this row
523                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    /// Get a single value at (row, col) by locating the correct `NodeGroup`
536    /// and `ColumnChunk`.
537    pub fn get_value(&self, row: usize, col: usize) -> Option<&Value> {
538        self.get_value_with_snapshot(row, col, None, &[])
539    }
540
541    /// Get a single value with MVCC snapshot isolation.
542    ///
543    /// Checks `VersionInfo` for insert/delete visibility and `UpdateInfo`
544    /// version chains when `snapshot_ts` is provided.
545    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    /// Reconstruct column-major data (`Vec<Vec<Value>>`) from all node groups.
562    ///
563    /// Used by the processor (`resolve_scan_data`) for backward compatibility.
564    pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
565        self.to_column_major_data_with_predicate(None)
566    }
567
568    /// Like `to_column_major_data`, but applies an optional zone map predicate
569    /// `(col_idx, op_string, val)` to skip entire node groups.
570    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]; // Avoid allocating self.num_rows if we skip chunks
573
574        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; // Skip this entire node group
581                }
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    /// Like `to_column_major_data`, but with MVCC snapshot isolation.
598    ///
599    /// When `snapshot_ts` is `Some(...)`, rows inserted/deleted by transactions
600    /// committed after `snapshot_ts` are excluded, and versioned updates are
601    /// resolved to the value visible at that snapshot.
602    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    /// Like `to_column_major_data_with_snapshot`, but applies an optional zone
625    /// map predicate to skip entire node groups.
626    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    /// Like `to_column_major_data_with_predicate`, but additionally returns the
659    /// internal node id (global row offset) for every emitted row, in the same
660    /// order as the returned column data. This is the node id space used by the
661    /// processor's extend/insert/join operators (`<var>._id`).
662    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    /// Like `to_column_major_data_with_snapshot_and_predicate`, but additionally
695    /// returns the internal node id (global row offset) for every emitted row,
696    /// in the same order as the returned column data.
697    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    /// Binary-search for the node group that contains `row`.
732    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
746/// Convert a Value to its string representation for use as a hash index key.
747fn 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
768/// Coerce a row's values to the table's declared column logical types.
769///
770/// Constant evaluation (e.g. CREATE `{id: 41}`) produces `Value::Int64`
771/// literals regardless of the target column type. A UINT64 column must store
772/// `Value::UInt64`: the scan builds its Arrow type from the physical type of
773/// the column, and the ART primary-key index encodes signed and unsigned keys
774/// differently. Mixed signed/unsigned keys would corrupt range scans.
775fn 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/// A relationship (edge) table with CSR (Compressed Sparse Row) adjacency storage.
804///
805/// Each edge connects a source node to a destination node and may carry
806/// a set of property values (one per column in `columns`).
807///
808/// # Storage layout
809///
810/// - `edges` — flat edge list: `edge_idx → (src_offset, dst_offset)`
811/// - `fwd_adj` — forward index: `src_offset → Vec<(dst_offset, edge_idx)>`
812/// - `rev_adj` — reverse index: `dst_offset → Vec<(src_offset, edge_idx)>`
813/// - `properties` — column-major property storage: `properties[col_idx][edge_idx]`
814#[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    /// Flat edge list: edge_idx → (src_offset, dst_offset).
823    pub edges: Vec<(u64, u64)>,
824    /// Forward CSR adjacency: src_offset → [(dst_offset, edge_idx), ...].
825    pub fwd_adj: HashMap<u64, Vec<(u64, usize)>>,
826    /// Reverse CSR adjacency: dst_offset → [(src_offset, edge_idx), ...].
827    pub rev_adj: HashMap<u64, Vec<(u64, usize)>>,
828    /// Specialized CSR Index for fast graph traversals.
829    pub csr_index: Option<CsrIndex>,
830    /// Column-major property storage: properties[col_idx][edge_idx].
831    pub properties: Vec<Vec<Value>>,
832    /// Set when an UPDATE/DELETE touches the table. The durable column mirror
833    /// (see `persistence.rs`) performs a full rewrite when this flag is set.
834    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    /// Insert a relationship (edge) between two nodes with property values.
863    ///
864    /// `from` and `to` are the node offsets of the source and destination
865    /// nodes within their respective tables.
866    ///
867    /// Returns an error if the number of values doesn't match the number
868    /// of property columns.
869    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        // Update forward adjacency.
882        self.fwd_adj.entry(from).or_default().push((to, edge_idx));
883
884        // Update reverse adjacency.
885        self.rev_adj.entry(to).or_default().push((from, edge_idx));
886        // Store property values.
887        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    /// Batch insert multiple relations efficiently.
895    /// Each tuple is (from_offset, to_offset, property_values).
896    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        // Validate all rows
904        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        // Pre-allocate
916        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        // Batch append
924        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    /// Delete an edge by its index. Marks the edge as deleted by removing it from adjacency lists
939    /// and setting its properties to Null.
940    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            // Already deleted
948            return Ok(());
949        }
950
951        // Remove from fwd_adj
952        if let Some(adj) = self.fwd_adj.get_mut(&src) {
953            adj.retain(|&(_, idx)| idx != edge_idx);
954        }
955
956        // Remove from rev_adj
957        if let Some(adj) = self.rev_adj.get_mut(&dst) {
958            adj.retain(|&(_, idx)| idx != edge_idx);
959        }
960
961        // Tombstone the edge
962        self.edges[edge_idx] = (u64::MAX, u64::MAX);
963        self.persistence_dirty = true;
964
965        // Nullify properties
966        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    /// Update a single cell (edge property) with a new value.
976    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    /// Insert a row of values (legacy alias that treats all columns as properties).
990    /// Only the first two values are treated as (from, to) if the table has
991    /// at least 2 columns; otherwise they are stored as pure properties.
992    pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
993        // If there are at least 2 "structural" columns (src_id, dst_id) plus
994        // property columns, we assume the first two values are the node offsets.
995        // This preserves backward compatibility with the old flat API.
996        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        // We treat the values as plain properties and use sequential edge IDs
1006        // as (from, to) placeholders. Real callers should use `insert_rel`.
1007        let from = self.num_rows;
1008        let to = self.num_rows;
1009        self.insert_rel(from, to, values)?;
1010        Ok(0) // insert_row on RelTable doesn't have a meaningful row offset right now
1011    }
1012
1013    /// Scan the forward adjacency list for a given source node.
1014    ///
1015    /// Returns a list of `(dst_offset, edge_idx)` pairs, or an empty vec
1016    /// if the node has no outgoing edges.
1017    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    /// Scan the reverse adjacency list for a given destination node.
1022    ///
1023    /// Returns a list of `(src_offset, edge_idx)` pairs, or an empty vec
1024    /// if the node has no incoming edges.
1025    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    /// Get all outgoing edges from a source node as `(dst_offset, property_values)`.
1030    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    /// Get all incoming edges to a destination node as `(src_offset, property_values)`.
1041    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    /// Get the property values for a specific edge by index.
1052    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    /// Get all values for a given property column (by index) as a slice.
1064    pub fn get_column(&self, col_idx: usize) -> Option<&[Value]> {
1065        self.properties.get(col_idx).map(|v| v.as_slice())
1066    }
1067
1068    /// Reconstruct column-major data from properties for backward compatibility.
1069    pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
1070        self.properties.clone()
1071    }
1072}
1073
1074/// A collection of tables managed by the storage engine.
1075///
1076/// Uses `DashMap` internally for lock-free concurrent reads.
1077/// Write operations synchronize on individual entries rather than
1078/// the entire catalog, allowing concurrent writers to different
1079/// tables to proceed in parallel.
1080#[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    /// Map from table name to table ID for node tables.
1086    node_name_to_id: DashMap<String, u64>,
1087    /// Map from table name to table ID for rel tables.
1088    rel_name_to_id: DashMap<String, u64>,
1089    /// Map from index name to index ID for vector indexes.
1090    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    /// Recreate a node table at a specific table ID (used when restoring a
1108    /// persisted catalog during recovery). Advances `next_table_id` so that
1109    /// subsequent auto-assigned IDs never collide with restored ones.
1110    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    /// Recreate a rel table at a specific table ID (used when restoring a
1133    /// persisted catalog during recovery). Advances `next_table_id` so that
1134    /// subsequent auto-assigned IDs never collide with restored ones.
1135    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    /// Advance `next_table_id` to be strictly greater than `table_id` so
1151    /// restored IDs are never re-issued by `create_node_table`/`create_rel_table`.
1152    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    /// Check if a node has any incident edges.
1204    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    /// Delete all incident edges for a given node.
1225    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    /// Get the number of rows in a node table by name.
1260    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    /// Remove a node table by name. Returns true if the table existed.
1265    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    /// Create a new vector index in the catalog.
1277    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    /// Get a vector index by its ID.
1293    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    /// Get a vector index by name.
1298    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    /// Get a mutable vector index by name.
1304    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    /// Get a mutable vector index by ID.
1313    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    /// Remove a vector index by name. Returns true if the index existed.
1321    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    /// Get all vector indexes.
1333    pub fn all_vector_indexes(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, VectorIndexTable>> {
1334        self.vector_indexes.iter().collect()
1335    }
1336
1337    /// Create an ART (Adaptive Radix Tree) index on a node table's PK column.
1338    ///
1339    /// Creates a new `ArtPrimaryKeyIndex`, backfills it with all existing rows,
1340    /// and attaches it to the `NodeTable`.
1341    ///
1342    /// The `index_name` is used as the BufferManager file name for persistence.
1343    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        // Backfill existing rows
1357        let pk_col = table.primary_key_column;
1358        // Scan all rows via to_column_major_data for backfill
1359        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    /// Drop the ART index from a node table.
1375    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    /// Get a reference to the ART index for a node table (via table name).
1385    /// Returns `None` if the table has no ART index or doesn't exist.
1386    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    /// Check if a node table has an ART index.
1392    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    /// Remove a rel table by name. Returns true if the table existed.
1399    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// ---------------------------------------------------------------------------
1412// Tests
1413// ---------------------------------------------------------------------------
1414
1415#[cfg(test)]
1416mod tests {
1417    use super::*;
1418    use crate::column_chunk::NODE_GROUP_SIZE;
1419
1420    // ==================== NodeTable tests ====================
1421
1422    #[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        // Insert NODE_GROUP_SIZE + 1 rows to force a second node group
1541        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        // Scan should still return all values
1549        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    // ==================== RelTable (CSR) tests ====================
1557
1558    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        // Forward adjacency from node 0
1604        let fwd = rel.scan_adj_list(0);
1605        assert_eq!(fwd.len(), 2);
1606        assert_eq!(fwd[0], (1, 0)); // (dst=1, edge_idx=0)
1607        assert_eq!(fwd[1], (2, 1)); // (dst=2, edge_idx=1)
1608
1609        // Forward from node 1
1610        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        // Node 5 has two incoming edges
1624        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        // insert_row treats values as properties with sequential edge IDs
1687        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)); // sequential from=0, to=0
1690        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)]); // 1 value, expected 2
1697        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    // ==================== TableCatalog tests ====================
1730
1731    #[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}