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::column::Column;
6use crate::column_chunk::ColumnChunk;
7use crate::csr::CsrIndex;
8use crate::index::HashIndex;
9use crate::node_group::NodeGroup;
10use crate::spiller::Spiller;
11use crate::vector_index::VectorIndexTable;
12use akar_common::error::StorageError;
13use akar_common::types::{LogicalTypeID, Value, pk_value_to_string};
14use akar_vector::hnsw::DistanceMetric;
15use dashmap::DashMap;
16use std::collections::HashMap;
17use std::sync::Arc;
18
19/// A column definition within a table.
20#[derive(Debug, Clone)]
21pub struct ColumnDefinition {
22    pub name: String,
23    pub logical_type: LogicalTypeID,
24    pub is_primary_key: bool,
25    pub compression: akar_common::enums::CompressionType,
26}
27
28/// A node table stores properties for a node label using NodeGroup-based
29/// columnar storage. Data is held in-memory as `NodeGroup`s; when a group
30/// reaches `NODE_GROUP_SIZE` rows a new group is automatically created.
31///
32/// Primary key uniqueness is enforced via an in-memory `HashIndex` that maps
33/// PK values to row offsets. For persistent indexes, use `OnDiskHashIndex`
34/// alongside the L1 cache (see `index.rs`).
35///
36/// Optionally, an `ArtPrimaryKeyIndex` can be attached for range-scan
37/// support on the primary key column (see `create_art_index`).
38#[derive(Debug, Clone)]
39pub struct NodeTable {
40    pub table_id: u64,
41    pub name: String,
42    pub columns: Vec<ColumnDefinition>,
43    pub primary_key_column: usize,
44    pub num_rows: u64,
45    /// NodeGroup-based columnar storage. Each group holds up to
46    /// `NODE_GROUP_SIZE` rows across all columns.
47    pub node_groups: Vec<NodeGroup>,
48    /// In-memory hash index for primary key → row lookup and dedup.
49    /// Stores the PK value (as a string representation) mapped to row offset.
50    pub hash_index: HashIndex<String>,
51    /// Optional ART (Adaptive Radix Tree) index for PK range scans.
52    /// When present, `insert_row()` also updates this index automatically.
53    pub art_index: Option<ArtPrimaryKeyIndex>,
54    /// Set when an UPDATE/DELETE touches the table. The durable column mirror
55    /// (see `persistence.rs`) performs a full rewrite when this flag is set.
56    pub persistence_dirty: bool,
57    /// Optional spiller for memory-bounded bulk ingest. When set, newly
58    /// created NodeGroups spill to disk once their buffer exceeds the memory
59    /// threshold, and the spilled rows are merged back into the in-memory
60    /// columns at the end of the insert operation (P51.44).
61    spiller: Option<Arc<Spiller>>,
62}
63
64/// Sentinel for `NodeTable::primary_key_column` when a node table has no PK
65/// column. SQL always requires a PK (binder rejects `CREATE NODE TABLE` without
66/// one), so via the SQL path this is unreachable; the sentinel only covers
67/// internally/test-constructed tables and keeps "no PK" explicit instead of
68/// silently defaulting to column 0 (P49.2).
69pub const NO_PRIMARY_KEY: usize = usize::MAX;
70
71impl NodeTable {
72    pub fn new(table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> Self {
73        // SQL guarantees a PK exists (binder/mod.rs:861 rejects tables without
74        // one), so the fallback is unreachable in production. Use an explicit
75        // sentinel instead of a silent 0 so a table with no PK column never
76        // accidentally dedups on column 0 (P49.2).
77        let primary_key_column = columns.iter().position(|c| c.is_primary_key).unwrap_or(NO_PRIMARY_KEY);
78        Self {
79            table_id,
80            name,
81            columns,
82            primary_key_column,
83            num_rows: 0,
84            node_groups: Vec::new(),
85            hash_index: HashIndex::new(),
86            art_index: None,
87            persistence_dirty: false,
88            spiller: None,
89        }
90    }
91
92    /// Attach a spiller to this table so bulk inserts spill to disk once a
93    /// NodeGroup's buffer exceeds the memory threshold (P51.44).
94    pub fn set_spiller(&mut self, spiller: Option<Arc<Spiller>>) {
95        self.spiller = spiller;
96    }
97
98    /// Widen the table schema with a new column (ALTER TABLE ADD). Every
99    /// existing row gets a NULL in the new column and all node groups are
100    /// widened so scans emit it (P53.37). The main DDL catalog is updated by
101    /// the caller; without this storage-side mirror the column silently
102    /// vanishes from scans and the export/projection paths drift.
103    pub fn add_column(&mut self, column: ColumnDefinition) {
104        if self.columns.iter().any(|c| c.name.eq_ignore_ascii_case(&column.name)) {
105            return;
106        }
107        self.columns.push(column);
108        for group in &mut self.node_groups {
109            let mut chunk = ColumnChunk::new();
110            for _ in 0..group.num_nodes {
111                chunk.append(Value::Null);
112            }
113            group.columns.push(chunk);
114        }
115        self.persistence_dirty = true;
116    }
117
118    /// Insert a row of values into the table.
119    ///
120    /// Appends to the current `NodeGroup`; auto-creates a new group when the
121    /// current one is full (reaches `NODE_GROUP_SIZE` rows).
122    ///
123    /// If the table has a primary key column, checks for duplicates and rejects
124    /// rows with already-existing PK values. The hash index is updated after
125    /// a successful insert.
126    ///
127    /// When `txn_id` is `Some(...)`, the insert is recorded in VersionInfo
128    /// for MVCC snapshot isolation.
129    ///
130    /// Returns an error if the number of values doesn't match the number of columns,
131    /// or if a duplicate primary key value is detected.
132    pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
133        self.insert_row_with_txn(values, None)
134    }
135
136    /// Insert a row with an optional transaction ID for MVCC tracking.
137    pub fn insert_row_with_txn(&mut self, mut values: Vec<Value>, txn_id: Option<u64>) -> Result<u64, StorageError> {
138        if values.len() != self.columns.len() {
139            return Err(StorageError::Page(format!(
140                "Column count mismatch: expected {} values, got {}",
141                self.columns.len(),
142                values.len()
143            )));
144        }
145
146        // Reject NULL primary key values
147        if self.primary_key_column < self.columns.len() {
148            let pk_value = &values[self.primary_key_column];
149            if matches!(pk_value, Value::Null) {
150                return Err(StorageError::Page(format!(
151                    "NULL value not allowed for primary key column '{}' in table '{}'",
152                    self.columns[self.primary_key_column].name, self.name
153                )));
154            }
155        }
156
157        // Coerce literal values to the declared column types (P48.12): constant
158        // evaluation (e.g. CREATE `{id: 41}`) produces Int64 literals regardless
159        // of the target column, and a UINT64 column must store Value::UInt64 so
160        // the scan builds the correct Arrow type instead of dropping the value.
161        coerce_values_to_columns(&mut values, &self.columns)?;
162
163        // Check primary key uniqueness
164        if self.primary_key_column < self.columns.len() {
165            let pk_value = &values[self.primary_key_column];
166            let pk_key = pk_value_to_string(pk_value);
167            if self.hash_index.lookup(&pk_key).is_some() {
168                return Err(StorageError::Index(format!(
169                    "Duplicate primary key value: '{pk_key}' in table '{}'",
170                    self.name
171                )));
172            }
173        }
174
175        // Get or create the current node group.
176        let num_cols = self.columns.len();
177        if self.node_groups.is_empty() || self.node_groups.last().unwrap().is_full() {
178            let start_offset = self.num_rows;
179            let mut new_group = NodeGroup::new(num_cols, start_offset);
180            // Enable version info if MVCC tracking is requested
181            if txn_id.is_some() {
182                new_group.enable_version_info();
183            }
184            if let Some(ref spiller) = self.spiller {
185                new_group.set_spiller(spiller.clone());
186            }
187            self.node_groups.push(new_group);
188        }
189
190        let current = self.node_groups.last_mut().unwrap();
191        // Enable version info on existing group if needed
192        if txn_id.is_some() {
193            current.enable_version_info();
194        }
195        current.append_row_with_txn(values.clone(), txn_id)?;
196        // Merge any spilled rows back into the in-memory group so scans and
197        // the column mirror stay authoritative after a memory-bounded ingest.
198        current.restore_spilled()?;
199        self.num_rows += 1;
200
201        // Update hash index with the PK value for this row
202        if self.primary_key_column < self.columns.len() {
203            let pk_value = &values[self.primary_key_column];
204            let pk_key = pk_value_to_string(pk_value);
205            self.hash_index.insert(pk_key, self.num_rows - 1);
206
207            // Also update ART index if present
208            if let Some(ref mut art_idx) = self.art_index
209                && let Some(art_key) = ArtKey::from_value(pk_value)
210            {
211                art_idx.insert(&art_key, self.num_rows - 1);
212            }
213        }
214
215        Ok(self.num_rows - 1)
216    }
217
218    /// Batch insert multiple rows efficiently.
219    /// Validates PK uniqueness, pre-allocates node groups, and bulk-appends.
220    /// When `txn_id` is `Some(...)`, inserts are recorded in VersionInfo for MVCC.
221    pub fn insert_rows_batch(&mut self, rows: &[Vec<Value>]) -> Result<u64, StorageError> {
222        self.insert_rows_batch_with_txn(rows, None)
223    }
224
225    /// Batch insert with optional MVCC tracking.
226    pub fn insert_rows_batch_with_txn(
227        &mut self,
228        rows: &[Vec<Value>],
229        txn_id: Option<u64>,
230    ) -> Result<u64, StorageError> {
231        if rows.is_empty() {
232            return Ok(0);
233        }
234        let num_cols = self.columns.len();
235
236        // Validate all rows first
237        for (i, row) in rows.iter().enumerate() {
238            if row.len() != num_cols {
239                return Err(StorageError::Page(format!(
240                    "Row {} column count mismatch: expected {} values, got {}",
241                    i,
242                    num_cols,
243                    row.len()
244                )));
245            }
246            // Reject NULL primary key values
247            if self.primary_key_column < num_cols {
248                let pk_value = &row[self.primary_key_column];
249                if matches!(pk_value, Value::Null) {
250                    return Err(StorageError::Page(format!(
251                        "NULL value not allowed for primary key column '{}' in table '{}'",
252                        self.columns[self.primary_key_column].name, self.name
253                    )));
254                }
255            }
256            // Check PK uniqueness
257            if self.primary_key_column < num_cols {
258                let pk_key = pk_value_to_string(&row[self.primary_key_column]);
259                if self.hash_index.lookup(&pk_key).is_some() {
260                    return Err(StorageError::Index(format!(
261                        "Duplicate primary key value: '{pk_key}' in table '{}'",
262                        self.name
263                    )));
264                }
265            }
266        }
267
268        let start_offset = self.num_rows;
269        let total_new = rows.len();
270
271        // Coerce literal values to the declared column types (P48.12), mirroring
272        // `insert_row_with_txn`. This must happen before appending and index
273        // updates so PK hash/ART keys use the coerced (UInt64) encoding.
274        let mut coerced_rows = rows.to_vec();
275        for row in &mut coerced_rows {
276            coerce_values_to_columns(row, &self.columns)?;
277        }
278        let rows: &[Vec<Value>] = &coerced_rows;
279
280        // Ensure we have a node group with enough capacity
281        if self.node_groups.is_empty() || self.node_groups.last().unwrap().is_full() {
282            let off = if self.node_groups.is_empty() {
283                start_offset
284            } else {
285                self.num_rows
286            };
287            let mut new_group = NodeGroup::new(num_cols, off);
288            if txn_id.is_some() {
289                new_group.enable_version_info();
290            }
291            if let Some(ref spiller) = self.spiller {
292                new_group.set_spiller(spiller.clone());
293            }
294            self.node_groups.push(new_group);
295        }
296
297        // Append to the last group (spilling into new groups if needed)
298        let mut inserted = 0usize;
299        while inserted < total_new {
300            let current = self.node_groups.last_mut().unwrap();
301            if txn_id.is_some() {
302                current.enable_version_info();
303            }
304            let rem = current.remaining();
305            let take = (total_new - inserted).min(rem);
306            for row in &rows[inserted..inserted + take] {
307                current.append_row_with_txn(row.clone(), txn_id)?;
308            }
309            self.num_rows += take as u64;
310            inserted += take;
311            if inserted < total_new {
312                let off = self.num_rows;
313                let mut new_group = NodeGroup::new(num_cols, off);
314                if txn_id.is_some() {
315                    new_group.enable_version_info();
316                }
317                if let Some(ref spiller) = self.spiller {
318                    new_group.set_spiller(spiller.clone());
319                }
320                self.node_groups.push(new_group);
321            }
322        }
323
324        // Merge any spilled rows back into the in-memory groups so scans and
325        // the column mirror stay authoritative after a memory-bounded ingest.
326        for group in &mut self.node_groups {
327            group.restore_spilled()?;
328        }
329
330        // Batch update indexes
331        for (i, row) in rows.iter().enumerate() {
332            if self.primary_key_column < num_cols {
333                let pk_key = pk_value_to_string(&row[self.primary_key_column]);
334                self.hash_index.insert(pk_key, start_offset + i as u64);
335                if let Some(ref mut art_idx) = self.art_index
336                    && let Some(art_key) = ArtKey::from_value(&row[self.primary_key_column])
337                {
338                    art_idx.insert(&art_key, start_offset + i as u64);
339                }
340            }
341        }
342
343        Ok(rows.len() as u64)
344    }
345
346    /// Look up a row offset by its primary key value.
347    ///
348    /// Returns `Some(row_offset)` if the PK exists, or `None` if not found.
349    /// Uses the in-memory hash index for O(1) lookup.
350    pub fn lookup_by_pk(&self, pk_value: &Value) -> Option<u64> {
351        let pk_key = pk_value_to_string(pk_value);
352        self.hash_index.lookup(&pk_key)
353    }
354
355    /// Batch look up row offsets for multiple primary key values.
356    ///
357    /// Returns a `Vec<Option<u64>>` parallel to the input, where each element
358    /// is `Some(row_offset)` if the PK exists, or `None` if not found.
359    /// Uses the in-memory hash index for O(1) per-key lookup, avoiding
360    /// per-row method-call overhead by inlining the lookup logic.
361    pub fn lookup_by_pk_batch(&self, pk_values: &[Value]) -> Vec<Option<u64>> {
362        pk_values
363            .iter()
364            .map(|pk_value| {
365                let pk_key = pk_value_to_string(pk_value);
366                self.hash_index.lookup(&pk_key)
367            })
368            .collect()
369    }
370
371    /// Perform a range scan on the primary key column using the ART index.
372    ///
373    /// Returns up to `max_results` row offsets for keys within `[lower, upper]`
374    /// (respecting inclusivity flags). Returns an empty vec if no ART index
375    /// exists or no keys match.
376    ///
377    /// This is the bridge function called by `PhysicalArtIndexRangeScan`.
378    pub fn lookup_by_pk_range(
379        &self,
380        lower: Option<&Value>,
381        lower_inclusive: bool,
382        upper: Option<&Value>,
383        upper_inclusive: bool,
384        max_results: u64,
385    ) -> Vec<u64> {
386        match &self.art_index {
387            Some(idx) => {
388                let lower_key = lower.and_then(ArtKey::from_value);
389                let upper_key = upper.and_then(ArtKey::from_value);
390                idx.range_scan(
391                    lower_key.as_ref(),
392                    lower_inclusive,
393                    upper_key.as_ref(),
394                    upper_inclusive,
395                    max_results,
396                )
397            }
398            None => Vec::new(),
399        }
400    }
401
402    /// Scan all values for a given column across all node groups.
403    ///
404    /// Returns a flat `Vec<Value>` containing values from `start` to
405    /// `start + count` (or fewer if the end of the table is reached).
406    ///
407    /// If `snapshot_ts` is `Some(...)`, performs MVCC snapshot isolation:
408    /// rows inserted/deleted by transactions committed after `snapshot_ts`
409    /// are excluded, and versioned updates are resolved.
410    pub fn scan_column(
411        &self,
412        col_idx: usize,
413        start: u64,
414        count: u64,
415        snapshot_ts: Option<u64>,
416        commit_history: &[(u64, u64)],
417    ) -> Vec<Value> {
418        if col_idx >= self.columns.len() || start >= self.num_rows {
419            return Vec::new();
420        }
421        let end = (start + count).min(self.num_rows);
422        let mut result = Vec::with_capacity((end - start) as usize);
423
424        // Find the first node group containing `start`.
425        let group_start = self.find_group(start);
426        let mut remaining = end - start;
427
428        for g_idx in group_start..self.node_groups.len() {
429            if remaining == 0 {
430                break;
431            }
432            let group = &self.node_groups[g_idx];
433            let local_start = if g_idx == group_start {
434                (start - group.start_offset) as usize
435            } else {
436                0
437            };
438            let available = (group.num_nodes as usize).saturating_sub(local_start);
439            let take = available.min(remaining as usize);
440
441            for row in local_start..local_start + take {
442                let val = group.get_value_with_snapshot(row, col_idx, snapshot_ts, commit_history);
443                match val {
444                    Some(v) => result.push(v.clone()),
445                    None => result.push(Value::Null),
446                }
447            }
448            remaining -= take as u64;
449        }
450
451        result
452    }
453
454    /// Rebuild the table's in-memory state from rows loaded off the durable
455    /// column mirror (see `persistence.rs`).
456    ///
457    /// Populates `node_groups`, `num_rows`, the PK hash index, and the ART
458    /// index directly, bypassing PK uniqueness checks so that soft-deleted
459    /// rows (whose PK is `Null`) can be restored at their original row
460    /// offsets.
461    pub fn load_persisted_rows(&mut self, rows: Vec<Vec<Value>>) -> Result<(), StorageError> {
462        let num_cols = self.columns.len();
463        self.node_groups.clear();
464        self.hash_index.clear();
465
466        let mut offset = 0u64;
467        let mut group = NodeGroup::new(num_cols, offset);
468        for row in &rows {
469            if row.len() != num_cols {
470                return Err(StorageError::Page(format!(
471                    "load_persisted_rows: expected {num_cols} values, got {}",
472                    row.len()
473                )));
474            }
475            group.append_row_with_txn(row.clone(), None)?;
476            offset += 1;
477            if group.is_full() {
478                self.node_groups.push(group);
479                group = NodeGroup::new(num_cols, offset);
480            }
481        }
482        if group.num_nodes > 0 {
483            self.node_groups.push(group);
484        }
485        self.num_rows = offset;
486
487        // Rebuild the PK hash index (skip soft-deleted rows with a Null PK).
488        if self.primary_key_column < num_cols {
489            for (row_idx, values) in rows.iter().enumerate() {
490                let pk = &values[self.primary_key_column];
491                if matches!(pk, Value::Null) {
492                    continue;
493                }
494                let key = pk_value_to_string(pk);
495                self.hash_index.insert(key, row_idx as u64);
496            }
497        }
498
499        // Rebuild the ART index if present.
500        if self.art_index.is_some() && self.primary_key_column < num_cols {
501            if let Some(art) = &mut self.art_index {
502                art.clear();
503                for (row_idx, values) in rows.iter().enumerate() {
504                    let pk = &values[self.primary_key_column];
505                    if matches!(pk, Value::Null) {
506                        continue;
507                    }
508                    if let Some(key) = ArtKey::from_value(pk) {
509                        art.insert(&key, row_idx as u64);
510                    }
511                }
512            }
513        }
514        Ok(())
515    }
516
517    /// Update a single cell (row, column) with a new value.
518    pub fn update_cell(&mut self, row_idx: u64, col_idx: usize, value: Value) -> Result<(), StorageError> {
519        if col_idx >= self.columns.len() {
520            return Err(StorageError::Page(format!("Column index {col_idx} out of range")));
521        }
522        if row_idx >= self.num_rows {
523            return Err(StorageError::Page(format!(
524                "Row index {row_idx} out of range (num_rows={})",
525                self.num_rows
526            )));
527        }
528        self.persistence_dirty = true;
529
530        let mut offset = 0u64;
531        for group in &mut self.node_groups {
532            if row_idx < offset + group.num_nodes {
533                let local_row = (row_idx - offset) as usize;
534                if let Some(col_chunk) = group.columns.get_mut(col_idx) {
535                    col_chunk.set_value(local_row, value)?;
536                }
537                return Ok(());
538            }
539            offset += group.num_nodes;
540        }
541        Err(StorageError::Page(format!(
542            "Row index {row_idx} not found in any node group"
543        )))
544    }
545
546    /// Delete a row by its index. Marks the row as null by setting all its column
547    /// values to `Value::Null`. This is a soft delete — the row slot remains.
548    pub fn delete_row(&mut self, row_idx: u64) -> Result<(), StorageError> {
549        self.delete_row_with_txn(row_idx, None)
550    }
551
552    /// Delete a row with optional MVCC tracking.
553    ///
554    /// When `txn_id` is `Some(...)`, the delete is recorded in VersionInfo
555    /// for MVCC snapshot isolation.
556    pub fn delete_row_with_txn(&mut self, row_idx: u64, txn_id: Option<u64>) -> Result<(), StorageError> {
557        if row_idx >= self.num_rows {
558            return Err(StorageError::Page(format!(
559                "Row index {row_idx} out of range (num_rows={})",
560                self.num_rows
561            )));
562        }
563        self.persistence_dirty = true;
564
565        // Locate the node group containing this row
566        let mut offset = 0u64;
567        for group in &mut self.node_groups {
568            if row_idx < offset + group.num_nodes {
569                let local_row = (row_idx - offset) as usize;
570                // Record delete in VersionInfo if MVCC tracking is active
571                if let Some(txn) = txn_id {
572                    if let Some(ref vi) = group.version_info {
573                        vi.delete(txn, local_row as u32);
574                    }
575                }
576                // Capture the PK value before it is nulled so the in-memory
577                // PK indexes can be kept in sync (P52.16).
578                let pk_value = (self.primary_key_column < self.columns.len())
579                    .then(|| group.columns.get(self.primary_key_column))
580                    .flatten()
581                    .and_then(|chunk| chunk.get(local_row))
582                    .cloned();
583                // Set all columns to Null for this row
584                for col_chunk in &mut group.columns {
585                    let _ = col_chunk.set_value(local_row, Value::Null);
586                }
587                // Soft-deleted rows must no longer resolve via PK lookup, and
588                // the same PK must be re-insertable. Drop it from the hash and
589                // ART indexes (P52.16).
590                if let Some(pk) = pk_value
591                    && !matches!(pk, Value::Null)
592                {
593                    let pk_key = pk_value_to_string(&pk);
594                    self.hash_index.delete(&pk_key);
595                    if let Some(ref mut art_idx) = self.art_index
596                        && let Some(art_key) = ArtKey::from_value(&pk)
597                    {
598                        art_idx.delete(&art_key, row_idx);
599                    }
600                }
601                return Ok(());
602            }
603            offset += group.num_nodes;
604        }
605        Err(StorageError::Page(format!(
606            "Row index {row_idx} not found in any node group"
607        )))
608    }
609
610    /// Capture the full row (all columns) as serialized undo bytes.
611    /// Used by the write path to record `UndoType::Delete` records so a
612    /// rollback can restore a soft-deleted row (P52.18).
613    pub fn row_undo_bytes(&self, row_idx: u64) -> Vec<u8> {
614        let mut out = Vec::new();
615        for col in 0..self.columns.len() {
616            let val = self.get_value(row_idx as usize, col).cloned().unwrap_or(Value::Null);
617            out.extend_from_slice(&Column::serialize_value(&val));
618        }
619        out
620    }
621
622    /// Capture a single cell as serialized undo bytes.
623    /// Used to record `UndoType::Update` records for `SET` rollback (P52.18).
624    pub fn cell_undo_bytes(&self, row_idx: u64, col_idx: usize) -> Vec<u8> {
625        let val = self
626            .get_value(row_idx as usize, col_idx)
627            .cloned()
628            .unwrap_or(Value::Null);
629        Column::serialize_value(&val)
630    }
631
632    /// Get a single value at (row, col) by locating the correct `NodeGroup`
633    /// and `ColumnChunk`.
634    pub fn get_value(&self, row: usize, col: usize) -> Option<&Value> {
635        self.get_value_with_snapshot(row, col, None, &[])
636    }
637
638    /// Get a single value with MVCC snapshot isolation.
639    ///
640    /// Checks `VersionInfo` for insert/delete visibility and `UpdateInfo`
641    /// version chains when `snapshot_ts` is provided.
642    pub fn get_value_with_snapshot(
643        &self,
644        row: usize,
645        col: usize,
646        snapshot_ts: Option<u64>,
647        commit_history: &[(u64, u64)],
648    ) -> Option<&Value> {
649        if col >= self.columns.len() || row as u64 >= self.num_rows {
650            return None;
651        }
652        let group_idx = self.find_group(row as u64);
653        let group = self.node_groups.get(group_idx)?;
654        let local_row = row as u64 - group.start_offset;
655        group.get_value_with_snapshot(local_row as usize, col, snapshot_ts, commit_history)
656    }
657
658    /// Reconstruct column-major data (`Vec<Vec<Value>>`) from all node groups.
659    ///
660    /// Used by the processor (`resolve_scan_data`) for backward compatibility.
661    pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
662        self.to_column_major_data_with_predicate(None)
663    }
664
665    /// Like `to_column_major_data`, but applies an optional zone map predicate
666    /// `(col_idx, op_string, val)` to skip entire node groups.
667    pub fn to_column_major_data_with_predicate(&self, predicate: Option<(usize, &str, &Value)>) -> Vec<Vec<Value>> {
668        let num_cols = self.columns.len();
669        let mut result = vec![Vec::new(); num_cols]; // Avoid allocating self.num_rows if we skip chunks
670
671        for group in &self.node_groups {
672            if let Some((col_idx, op, val)) = predicate
673                && let Some(col_chunk) = group.columns.get(col_idx)
674            {
675                use crate::predicate::{ZoneMapCheckResult, check_zone_map};
676                if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
677                    continue; // Skip this entire node group
678                }
679            }
680
681            for row in 0..group.num_nodes as usize {
682                for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
683                    match group.get_value(row, col) {
684                        Some(v) => res_col.push(v.clone()),
685                        None => res_col.push(Value::Null),
686                    }
687                }
688            }
689        }
690
691        result
692    }
693
694    /// Like `to_column_major_data`, but with MVCC snapshot isolation.
695    ///
696    /// When `snapshot_ts` is `Some(...)`, rows inserted/deleted by transactions
697    /// committed after `snapshot_ts` are excluded, and versioned updates are
698    /// resolved to the value visible at that snapshot.
699    pub fn to_column_major_data_with_snapshot(
700        &self,
701        snapshot_ts: Option<u64>,
702        commit_history: &[(u64, u64)],
703    ) -> Vec<Vec<Value>> {
704        let num_cols = self.columns.len();
705        let mut result = vec![Vec::new(); num_cols];
706
707        for group in &self.node_groups {
708            for row in 0..group.num_nodes as usize {
709                for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
710                    match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
711                        Some(v) => res_col.push(v),
712                        None => res_col.push(Value::Null),
713                    }
714                }
715            }
716        }
717
718        result
719    }
720
721    /// Like `to_column_major_data_with_snapshot`, but applies an optional zone
722    /// map predicate to skip entire node groups.
723    pub fn to_column_major_data_with_snapshot_and_predicate(
724        &self,
725        predicate: Option<(usize, &str, &Value)>,
726        snapshot_ts: Option<u64>,
727        commit_history: &[(u64, u64)],
728    ) -> Vec<Vec<Value>> {
729        let num_cols = self.columns.len();
730        let mut result = vec![Vec::new(); num_cols];
731
732        for group in &self.node_groups {
733            if let Some((col_idx, op, val)) = predicate
734                && let Some(col_chunk) = group.columns.get(col_idx)
735            {
736                use crate::predicate::{ZoneMapCheckResult, check_zone_map};
737                if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
738                    continue;
739                }
740            }
741
742            for row in 0..group.num_nodes as usize {
743                for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
744                    match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
745                        Some(v) => res_col.push(v),
746                        None => res_col.push(Value::Null),
747                    }
748                }
749            }
750        }
751
752        result
753    }
754
755    /// Like `to_column_major_data_with_predicate`, but additionally returns the
756    /// internal node id (global row offset) for every emitted row, in the same
757    /// order as the returned column data. This is the node id space used by the
758    /// processor's extend/insert/join operators (`<var>._id`).
759    pub fn to_column_major_data_with_predicate_and_ids(
760        &self,
761        predicate: Option<(usize, &str, &Value)>,
762    ) -> (Vec<Vec<Value>>, Vec<u64>) {
763        let num_cols = self.columns.len();
764        let mut result = vec![Vec::new(); num_cols];
765        let mut ids = Vec::new();
766
767        for group in &self.node_groups {
768            if let Some((col_idx, op, val)) = predicate
769                && let Some(col_chunk) = group.columns.get(col_idx)
770            {
771                use crate::predicate::{ZoneMapCheckResult, check_zone_map};
772                if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
773                    continue;
774                }
775            }
776
777            for row in 0..group.num_nodes as usize {
778                ids.push(group.start_offset + row as u64);
779                for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
780                    match group.get_value(row, col) {
781                        Some(v) => res_col.push(v.clone()),
782                        None => res_col.push(Value::Null),
783                    }
784                }
785            }
786        }
787
788        (result, ids)
789    }
790
791    /// Like `to_column_major_data_with_snapshot_and_predicate`, but additionally
792    /// returns the internal node id (global row offset) for every emitted row,
793    /// in the same order as the returned column data.
794    pub fn to_column_major_data_with_snapshot_and_predicate_and_ids(
795        &self,
796        predicate: Option<(usize, &str, &Value)>,
797        snapshot_ts: Option<u64>,
798        commit_history: &[(u64, u64)],
799    ) -> (Vec<Vec<Value>>, Vec<u64>) {
800        let num_cols = self.columns.len();
801        let mut result = vec![Vec::new(); num_cols];
802        let mut ids = Vec::new();
803
804        for group in &self.node_groups {
805            if let Some((col_idx, op, val)) = predicate
806                && let Some(col_chunk) = group.columns.get(col_idx)
807            {
808                use crate::predicate::{ZoneMapCheckResult, check_zone_map};
809                if check_zone_map(&col_chunk.stats, op, val) == ZoneMapCheckResult::SkipScan {
810                    continue;
811                }
812            }
813
814            for row in 0..group.num_nodes as usize {
815                ids.push(group.start_offset + row as u64);
816                for (col, res_col) in result.iter_mut().enumerate().take(num_cols) {
817                    match group.get_value_owned_with_snapshot(row, col, snapshot_ts, commit_history) {
818                        Some(v) => res_col.push(v),
819                        None => res_col.push(Value::Null),
820                    }
821                }
822            }
823        }
824
825        (result, ids)
826    }
827
828    /// Binary-search for the node group that contains `row`.
829    fn find_group(&self, row: u64) -> usize {
830        self.node_groups
831            .binary_search_by_key(&row, |g| g.start_offset)
832            .unwrap_or_else(|i| if i == 0 { 0 } else { i - 1 })
833    }
834}
835
836/// Coerce a row's values to the table's declared column logical types.
837///
838/// Constant evaluation (e.g. CREATE `{id: 41}`) produces `Value::Int64`
839/// literals regardless of the target column type. A UINT64 column must store
840/// `Value::UInt64`: the scan builds its Arrow type from the physical type of
841/// the column, and the ART primary-key index encodes signed and unsigned keys
842/// differently. Mixed signed/unsigned keys would corrupt range scans.
843fn coerce_values_to_columns(values: &mut [Value], columns: &[ColumnDefinition]) -> Result<(), StorageError> {
844    for (i, col) in columns.iter().enumerate() {
845        let coerced = match (col.logical_type, &values[i]) {
846            (LogicalTypeID::UInt64, Value::Int64(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
847            (LogicalTypeID::UInt64, Value::Int32(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
848            (LogicalTypeID::UInt64, Value::Int16(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
849            (LogicalTypeID::UInt64, Value::Int8(x)) if *x >= 0 => Some(Value::UInt64(*x as u64)),
850            (LogicalTypeID::UInt64, Value::UInt32(x)) => Some(Value::UInt64(*x as u64)),
851            (LogicalTypeID::UInt64, Value::UInt16(x)) => Some(Value::UInt64(*x as u64)),
852            (LogicalTypeID::UInt64, Value::UInt8(x)) => Some(Value::UInt64(*x as u64)),
853            (LogicalTypeID::UInt64, Value::Int64(_))
854            | (LogicalTypeID::UInt64, Value::Int32(_))
855            | (LogicalTypeID::UInt64, Value::Int16(_))
856            | (LogicalTypeID::UInt64, Value::Int8(_)) => {
857                return Err(StorageError::Page(format!(
858                    "Cannot store negative value in UINT64 column '{}'",
859                    col.name
860                )));
861            }
862            _ => None,
863        };
864        if let Some(c) = coerced {
865            values[i] = c;
866        }
867    }
868    Ok(())
869}
870
871/// A relationship (edge) table with CSR (Compressed Sparse Row) adjacency storage.
872///
873/// Each edge connects a source node to a destination node and may carry
874/// a set of property values (one per column in `columns`).
875///
876/// # Storage layout
877///
878/// - `edges` — flat edge list: `edge_idx → (src_offset, dst_offset)`
879/// - `fwd_adj` — forward index: `src_offset → Vec<(dst_offset, edge_idx)>`
880/// - `rev_adj` — reverse index: `dst_offset → Vec<(src_offset, edge_idx)>`
881/// - `properties` — column-major property storage: `properties[col_idx][edge_idx]`
882#[derive(Debug, Clone)]
883pub struct RelTable {
884    pub table_id: u64,
885    pub name: String,
886    pub src_table_id: u64,
887    pub dst_table_id: u64,
888    pub columns: Vec<ColumnDefinition>,
889    pub num_rows: u64,
890    /// Flat edge list: edge_idx → (src_offset, dst_offset).
891    pub edges: Vec<(u64, u64)>,
892    /// Forward CSR adjacency: src_offset → [(dst_offset, edge_idx), ...].
893    pub fwd_adj: HashMap<u64, Vec<(u64, usize)>>,
894    /// Reverse CSR adjacency: dst_offset → [(src_offset, edge_idx), ...].
895    pub rev_adj: HashMap<u64, Vec<(u64, usize)>>,
896    /// Specialized CSR Index for fast graph traversals.
897    pub csr_index: Option<CsrIndex>,
898    /// Column-major property storage: properties[col_idx][edge_idx].
899    pub properties: Vec<Vec<Value>>,
900    /// Set when an UPDATE/DELETE touches the table. The durable column mirror
901    /// (see `persistence.rs`) performs a full rewrite when this flag is set.
902    pub persistence_dirty: bool,
903}
904
905impl RelTable {
906    pub fn new(
907        table_id: u64,
908        name: String,
909        src_table_id: u64,
910        dst_table_id: u64,
911        columns: Vec<ColumnDefinition>,
912    ) -> Self {
913        let num_cols = columns.len();
914        Self {
915            table_id,
916            name,
917            src_table_id,
918            dst_table_id,
919            columns,
920            num_rows: 0,
921            edges: Vec::new(),
922            fwd_adj: HashMap::new(),
923            rev_adj: HashMap::new(),
924            csr_index: None,
925            properties: vec![Vec::new(); num_cols],
926            persistence_dirty: false,
927        }
928    }
929
930    /// Widen the rel table schema with a new property column (ALTER TABLE ADD).
931    /// Existing edges get a NULL in the new property column (P53.37).
932    pub fn add_column(&mut self, column: ColumnDefinition) {
933        if self.columns.iter().any(|c| c.name.eq_ignore_ascii_case(&column.name)) {
934            return;
935        }
936        self.columns.push(column);
937        self.properties.push(vec![Value::Null; self.edges.len()]);
938        self.persistence_dirty = true;
939    }
940
941    /// Insert a relationship (edge) between two nodes with property values.
942    ///
943    /// `from` and `to` are the node offsets of the source and destination
944    /// nodes within their respective tables.
945    ///
946    /// Returns an error if the number of values doesn't match the number
947    /// of property columns.
948    pub fn insert_rel(&mut self, from: u64, to: u64, values: Vec<Value>) -> Result<(), StorageError> {
949        if values.len() != self.columns.len() {
950            return Err(StorageError::Page(format!(
951                "Column count mismatch: expected {} values, got {}",
952                self.columns.len(),
953                values.len()
954            )));
955        }
956
957        let edge_idx = self.edges.len();
958        self.edges.push((from, to));
959
960        // Update forward adjacency.
961        self.fwd_adj.entry(from).or_default().push((to, edge_idx));
962
963        // Update reverse adjacency.
964        self.rev_adj.entry(to).or_default().push((from, edge_idx));
965        // Store property values.
966        for (col_idx, val) in values.into_iter().enumerate() {
967            self.properties[col_idx].push(val);
968        }
969        self.num_rows += 1;
970        Ok(())
971    }
972
973    /// Batch insert multiple relations efficiently.
974    /// Each tuple is (from_offset, to_offset, property_values).
975    pub fn insert_rels_batch(&mut self, rels: &[(u64, u64, Vec<Value>)]) -> Result<u64, StorageError> {
976        if rels.is_empty() {
977            return Ok(0);
978        }
979        let num_cols = self.columns.len();
980        let total = rels.len();
981
982        // Validate all rows
983        for (i, (_, _, vals)) in rels.iter().enumerate() {
984            if vals.len() != num_cols {
985                return Err(StorageError::Page(format!(
986                    "Rel {} column count mismatch: expected {} values, got {}",
987                    i,
988                    num_cols,
989                    vals.len()
990                )));
991            }
992        }
993
994        // Pre-allocate
995        self.edges.reserve(total);
996        for col in &mut self.properties {
997            col.reserve(total);
998        }
999
1000        let _start_edge_idx = self.edges.len();
1001
1002        // Batch append
1003        for (from, to, vals) in rels {
1004            let edge_idx = self.edges.len();
1005            self.edges.push((*from, *to));
1006            self.fwd_adj.entry(*from).or_default().push((*to, edge_idx));
1007            self.rev_adj.entry(*to).or_default().push((*from, edge_idx));
1008            for (col_idx, val) in vals.iter().enumerate() {
1009                self.properties[col_idx].push(val.clone());
1010            }
1011        }
1012
1013        self.num_rows += total as u64;
1014        Ok(total as u64)
1015    }
1016
1017    /// Delete an edge by its index. Marks the edge as deleted by removing it from adjacency lists
1018    /// and setting its properties to Null.
1019    pub fn delete_edge(&mut self, edge_idx: usize) -> Result<(), StorageError> {
1020        if edge_idx >= self.edges.len() {
1021            return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
1022        }
1023
1024        let (src, dst) = self.edges[edge_idx];
1025        if src == u64::MAX {
1026            // Already deleted
1027            return Ok(());
1028        }
1029
1030        // Remove from fwd_adj
1031        if let Some(adj) = self.fwd_adj.get_mut(&src) {
1032            adj.retain(|&(_, idx)| idx != edge_idx);
1033        }
1034
1035        // Remove from rev_adj
1036        if let Some(adj) = self.rev_adj.get_mut(&dst) {
1037            adj.retain(|&(_, idx)| idx != edge_idx);
1038        }
1039
1040        // Tombstone the edge
1041        self.edges[edge_idx] = (u64::MAX, u64::MAX);
1042        self.persistence_dirty = true;
1043
1044        // Nullify properties
1045        for col in &mut self.properties {
1046            if edge_idx < col.len() {
1047                col[edge_idx] = Value::Null;
1048            }
1049        }
1050
1051        Ok(())
1052    }
1053
1054    /// Update a single cell (edge property) with a new value.
1055    pub fn update_cell(&mut self, edge_idx: usize, col_idx: usize, value: Value) -> Result<(), StorageError> {
1056        if col_idx >= self.columns.len() {
1057            return Err(StorageError::Page(format!("Column index {col_idx} out of range")));
1058        }
1059        if edge_idx >= self.properties[col_idx].len() {
1060            return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
1061        }
1062
1063        self.properties[col_idx][edge_idx] = value;
1064        self.persistence_dirty = true;
1065        Ok(())
1066    }
1067
1068    /// Capture an edge (src, dst) plus all property values as serialized undo
1069    /// bytes: `[src, dst, prop0..propN]`. Used to record `UndoType::Delete`
1070    /// records so a rollback can restore a deleted edge (P52.18).
1071    pub fn edge_undo_bytes(&self, edge_idx: usize) -> Vec<u8> {
1072        let (src, dst) = self.edges.get(edge_idx).copied().unwrap_or((u64::MAX, u64::MAX));
1073        let mut out = Vec::new();
1074        out.extend_from_slice(&Column::serialize_value(&Value::UInt64(src)));
1075        out.extend_from_slice(&Column::serialize_value(&Value::UInt64(dst)));
1076        for p in self.get_edge_properties(edge_idx) {
1077            out.extend_from_slice(&Column::serialize_value(&p));
1078        }
1079        out
1080    }
1081
1082    /// Capture a single edge property as serialized undo bytes.
1083    /// Used to record `UndoType::Update` records for `SET` rollback (P52.18).
1084    pub fn edge_cell_undo_bytes(&self, edge_idx: usize, col_idx: usize) -> Vec<u8> {
1085        let v = self
1086            .get_edge_properties(edge_idx)
1087            .get(col_idx)
1088            .cloned()
1089            .unwrap_or(Value::Null);
1090        Column::serialize_value(&v)
1091    }
1092
1093    /// Restore a tombstoned edge (rollback of a `DELETE` edge). Re-adds the
1094    /// edge to the forward/reverse adjacency lists and restores its properties
1095    /// (P52.18).
1096    pub fn restore_deleted_edge(
1097        &mut self,
1098        edge_idx: usize,
1099        src: u64,
1100        dst: u64,
1101        props: Vec<Value>,
1102    ) -> Result<(), StorageError> {
1103        if edge_idx >= self.edges.len() {
1104            return Err(StorageError::Page(format!("Edge index {edge_idx} out of range")));
1105        }
1106        self.edges[edge_idx] = (src, dst);
1107        self.fwd_adj.entry(src).or_default().push((dst, edge_idx));
1108        self.rev_adj.entry(dst).or_default().push((src, edge_idx));
1109        for (col_idx, val) in props.into_iter().enumerate() {
1110            if col_idx < self.properties.len() {
1111                if edge_idx < self.properties[col_idx].len() {
1112                    self.properties[col_idx][edge_idx] = val;
1113                } else {
1114                    self.properties[col_idx].push(val);
1115                }
1116            }
1117        }
1118        self.persistence_dirty = true;
1119        Ok(())
1120    }
1121
1122    /// Insert a row of values (legacy alias that treats all columns as properties).
1123    /// Only the first two values are treated as (from, to) if the table has
1124    /// at least 2 columns; otherwise they are stored as pure properties.
1125    pub fn insert_row(&mut self, values: Vec<Value>) -> Result<u64, StorageError> {
1126        // If there are at least 2 "structural" columns (src_id, dst_id) plus
1127        // property columns, we assume the first two values are the node offsets.
1128        // This preserves backward compatibility with the old flat API.
1129        let num_prop_cols = self.columns.len();
1130        if values.len() != num_prop_cols {
1131            return Err(StorageError::Page(format!(
1132                "Column count mismatch: expected {} values, got {}",
1133                num_prop_cols,
1134                values.len()
1135            )));
1136        }
1137
1138        // We treat the values as plain properties and use sequential edge IDs
1139        // as (from, to) placeholders. Real callers should use `insert_rel`.
1140        let from = self.num_rows;
1141        let to = self.num_rows;
1142        self.insert_rel(from, to, values)?;
1143        Ok(0) // insert_row on RelTable doesn't have a meaningful row offset right now
1144    }
1145
1146    /// Scan the forward adjacency list for a given source node.
1147    ///
1148    /// Returns a list of `(dst_offset, edge_idx)` pairs, or an empty vec
1149    /// if the node has no outgoing edges.
1150    pub fn scan_adj_list(&self, src_offset: u64) -> &[(u64, usize)] {
1151        self.fwd_adj.get(&src_offset).map(|v| v.as_slice()).unwrap_or(&[])
1152    }
1153
1154    /// Scan the reverse adjacency list for a given destination node.
1155    ///
1156    /// Returns a list of `(src_offset, edge_idx)` pairs, or an empty vec
1157    /// if the node has no incoming edges.
1158    pub fn scan_rev_adj_list(&self, dst_offset: u64) -> &[(u64, usize)] {
1159        self.rev_adj.get(&dst_offset).map(|v| v.as_slice()).unwrap_or(&[])
1160    }
1161
1162    /// Get all outgoing edges from a source node as `(dst_offset, property_values)`.
1163    pub fn get_outgoing_edges(&self, src_offset: u64) -> Vec<(u64, Vec<Value>)> {
1164        self.scan_adj_list(src_offset)
1165            .iter()
1166            .map(|&(dst, edge_idx)| {
1167                let props = self.get_edge_properties(edge_idx);
1168                (dst, props)
1169            })
1170            .collect()
1171    }
1172
1173    /// Get all incoming edges to a destination node as `(src_offset, property_values)`.
1174    pub fn get_incoming_edges(&self, dst_offset: u64) -> Vec<(u64, Vec<Value>)> {
1175        self.scan_rev_adj_list(dst_offset)
1176            .iter()
1177            .map(|&(src, edge_idx)| {
1178                let props = self.get_edge_properties(edge_idx);
1179                (src, props)
1180            })
1181            .collect()
1182    }
1183
1184    /// Get the property values for a specific edge by index.
1185    pub fn get_edge_properties(&self, edge_idx: usize) -> Vec<Value> {
1186        let mut props = Vec::with_capacity(self.columns.len());
1187        for col in &self.properties {
1188            match col.get(edge_idx) {
1189                Some(v) => props.push(v.clone()),
1190                None => props.push(Value::Null),
1191            }
1192        }
1193        props
1194    }
1195
1196    /// Get all values for a given property column (by index) as a slice.
1197    pub fn get_column(&self, col_idx: usize) -> Option<&[Value]> {
1198        self.properties.get(col_idx).map(|v| v.as_slice())
1199    }
1200
1201    /// Reconstruct column-major data from properties for backward compatibility.
1202    pub fn to_column_major_data(&self) -> Vec<Vec<Value>> {
1203        self.properties.clone()
1204    }
1205}
1206
1207/// A collection of tables managed by the storage engine.
1208///
1209/// Uses `DashMap` internally for lock-free concurrent reads.
1210/// Write operations synchronize on individual entries rather than
1211/// the entire catalog, allowing concurrent writers to different
1212/// tables to proceed in parallel.
1213#[derive(Debug, Default)]
1214pub struct TableCatalog {
1215    node_tables: DashMap<u64, NodeTable>,
1216    rel_tables: DashMap<u64, RelTable>,
1217    vector_indexes: DashMap<u64, VectorIndexTable>,
1218    /// Map from table name to table ID for node tables.
1219    node_name_to_id: DashMap<String, u64>,
1220    /// Map from table name to table ID for rel tables.
1221    rel_name_to_id: DashMap<String, u64>,
1222    /// Map from index name to index ID for vector indexes.
1223    vector_index_name_to_id: DashMap<String, u64>,
1224    next_table_id: std::sync::atomic::AtomicU64,
1225}
1226
1227impl TableCatalog {
1228    pub fn new() -> Self {
1229        Self::default()
1230    }
1231
1232    pub fn create_node_table(&self, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1233        let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1234        let table = NodeTable::new(table_id, name.clone(), columns);
1235        self.node_name_to_id.insert(name, table_id);
1236        self.node_tables.insert(table_id, table.clone());
1237        table
1238    }
1239
1240    /// Recreate a node table at a specific table ID (used when restoring a
1241    /// persisted catalog during recovery). Advances `next_table_id` so that
1242    /// subsequent auto-assigned IDs never collide with restored ones.
1243    pub fn create_node_table_with_id(&self, table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1244        self.bump_next_table_id(table_id);
1245        let table = NodeTable::new(table_id, name.clone(), columns);
1246        self.node_name_to_id.insert(name, table_id);
1247        self.node_tables.insert(table_id, table.clone());
1248        table
1249    }
1250
1251    pub fn create_rel_table(
1252        &self,
1253        name: String,
1254        src_table_id: u64,
1255        dst_table_id: u64,
1256        columns: Vec<ColumnDefinition>,
1257    ) -> RelTable {
1258        let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1259        let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1260        self.rel_name_to_id.insert(name, table_id);
1261        self.rel_tables.insert(table_id, table.clone());
1262        table
1263    }
1264
1265    /// Recreate a rel table at a specific table ID (used when restoring a
1266    /// persisted catalog during recovery). Advances `next_table_id` so that
1267    /// subsequent auto-assigned IDs never collide with restored ones.
1268    pub fn create_rel_table_with_id(
1269        &self,
1270        table_id: u64,
1271        name: String,
1272        src_table_id: u64,
1273        dst_table_id: u64,
1274        columns: Vec<ColumnDefinition>,
1275    ) -> RelTable {
1276        self.bump_next_table_id(table_id);
1277        let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1278        self.rel_name_to_id.insert(name, table_id);
1279        self.rel_tables.insert(table_id, table.clone());
1280        table
1281    }
1282
1283    /// Advance `next_table_id` to be strictly greater than `table_id` so
1284    /// restored IDs are never re-issued by `create_node_table`/`create_rel_table`.
1285    fn bump_next_table_id(&self, table_id: u64) {
1286        let mut next = self.next_table_id.load(std::sync::atomic::Ordering::SeqCst);
1287        while next <= table_id {
1288            match self.next_table_id.compare_exchange(
1289                next,
1290                table_id + 1,
1291                std::sync::atomic::Ordering::SeqCst,
1292                std::sync::atomic::Ordering::SeqCst,
1293            ) {
1294                Ok(_) => break,
1295                Err(current) => next = current,
1296            }
1297        }
1298    }
1299
1300    pub fn get_node_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1301        self.node_tables.get(&table_id)
1302    }
1303
1304    pub fn get_node_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1305        self.node_tables.get_mut(&table_id)
1306    }
1307
1308    pub fn get_node_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1309        let id = self.node_name_to_id.get(name)?;
1310        self.node_tables.get(&*id)
1311    }
1312
1313    pub fn get_node_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1314        let id = self.node_name_to_id.get(name)?;
1315        self.node_tables.get_mut(&*id)
1316    }
1317
1318    pub fn get_rel_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1319        self.rel_tables.get(&table_id)
1320    }
1321
1322    pub fn get_rel_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1323        self.rel_tables.get_mut(&table_id)
1324    }
1325
1326    pub fn get_rel_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1327        let id = self.rel_name_to_id.get(name)?;
1328        self.rel_tables.get(&*id)
1329    }
1330
1331    pub fn get_rel_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1332        let id = self.rel_name_to_id.get(name)?;
1333        self.rel_tables.get_mut(&*id)
1334    }
1335
1336    /// Check if a node has any incident edges.
1337    pub fn has_incident_edges(&self, table_id: u64, node_idx: u64) -> bool {
1338        for rel_table in self.rel_tables.iter() {
1339            if rel_table.src_table_id == table_id {
1340                if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1341                    if !edges.is_empty() {
1342                        return true;
1343                    }
1344                }
1345            }
1346            if rel_table.dst_table_id == table_id {
1347                if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1348                    if !edges.is_empty() {
1349                        return true;
1350                    }
1351                }
1352            }
1353        }
1354        false
1355    }
1356
1357    /// Delete all incident edges for a given node.
1358    pub fn detach_node(&self, table_id: u64, node_idx: u64) {
1359        for mut rel_table in self.rel_tables.iter_mut() {
1360            let mut edges_to_delete = Vec::new();
1361
1362            if rel_table.src_table_id == table_id {
1363                if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1364                    for &(_, edge_idx) in edges {
1365                        edges_to_delete.push(edge_idx);
1366                    }
1367                }
1368            }
1369
1370            if rel_table.dst_table_id == table_id {
1371                if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1372                    for &(_, edge_idx) in edges {
1373                        edges_to_delete.push(edge_idx);
1374                    }
1375                }
1376            }
1377
1378            for edge_idx in edges_to_delete {
1379                let _ = rel_table.delete_edge(edge_idx);
1380            }
1381        }
1382    }
1383
1384    pub fn all_node_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, NodeTable>> {
1385        self.node_tables.iter().collect()
1386    }
1387
1388    pub fn all_rel_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, RelTable>> {
1389        self.rel_tables.iter().collect()
1390    }
1391
1392    /// Get the number of rows in a node table by name.
1393    pub fn node_table_num_rows(&self, name: &str) -> u64 {
1394        self.get_node_table_by_name(name).map(|t| t.num_rows).unwrap_or(0)
1395    }
1396
1397    /// Remove a node table by name. Returns true if the table existed.
1398    pub fn drop_node_table(&self, name: &str) -> bool {
1399        if let Some(id) = self.node_name_to_id.get(name) {
1400            let table_id = *id;
1401            drop(id);
1402            self.node_name_to_id.remove(name);
1403            self.node_tables.remove(&table_id).is_some()
1404        } else {
1405            false
1406        }
1407    }
1408
1409    /// Create a new vector index in the catalog.
1410    pub fn create_vector_index(
1411        &self,
1412        name: String,
1413        table_name: String,
1414        column_name: String,
1415        metric: DistanceMetric,
1416        dimensions: u32,
1417    ) -> VectorIndexTable {
1418        let index_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1419        let table = VectorIndexTable::new(index_id, name.clone(), table_name, column_name, metric, dimensions);
1420        self.vector_index_name_to_id.insert(name, index_id);
1421        self.vector_indexes.insert(index_id, table.clone());
1422        table
1423    }
1424
1425    /// Get a vector index by its ID.
1426    pub fn get_vector_index(&self, index_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1427        self.vector_indexes.get(&index_id)
1428    }
1429
1430    /// Get a vector index by name.
1431    pub fn get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1432        let id = self.vector_index_name_to_id.get(name)?;
1433        self.vector_indexes.get(&*id)
1434    }
1435
1436    /// Get a mutable vector index by name.
1437    pub fn get_vector_index_by_name_mut(
1438        &self,
1439        name: &str,
1440    ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1441        let id = self.vector_index_name_to_id.get(name)?;
1442        self.vector_indexes.get_mut(&*id)
1443    }
1444
1445    /// Get a mutable vector index by ID.
1446    pub fn get_vector_index_mut(
1447        &self,
1448        index_id: u64,
1449    ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1450        self.vector_indexes.get_mut(&index_id)
1451    }
1452
1453    /// Remove a vector index by name. Returns true if the index existed.
1454    pub fn drop_vector_index(&self, name: &str) -> bool {
1455        if let Some(id) = self.vector_index_name_to_id.get(name) {
1456            let index_id = *id;
1457            drop(id);
1458            self.vector_index_name_to_id.remove(name);
1459            self.vector_indexes.remove(&index_id).is_some()
1460        } else {
1461            false
1462        }
1463    }
1464
1465    /// Get all vector indexes.
1466    pub fn all_vector_indexes(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, VectorIndexTable>> {
1467        self.vector_indexes.iter().collect()
1468    }
1469
1470    /// Rebuild a vector index from the current contents of its node table.
1471    ///
1472    /// The HNSW graph is re-populated with the live row ids and vectors, so
1473    /// INSERT/DELETE after `CREATE VECTOR INDEX` are always reflected (P52.38).
1474    pub fn refresh_vector_index(&self, index_id: u64) {
1475        let (table_name, column_name) = match self.vector_indexes.get(&index_id) {
1476            Some(vi) => (vi.table_name.clone(), vi.column_name.clone()),
1477            None => return,
1478        };
1479
1480        let col_idx = match self.get_node_table_by_name(&table_name) {
1481            Some(t) => match t.columns.iter().position(|c| c.name == column_name) {
1482                Some(idx) => idx,
1483                None => return,
1484            },
1485            None => return,
1486        };
1487
1488        // Collect (row_id, vector) pairs while holding the read reference,
1489        // then re-insert under a single mutable borrow of the index.
1490        let mut data: Vec<(usize, Vec<f64>)> = Vec::new();
1491        if let Some(table) = self.get_node_table_by_name(&table_name) {
1492            for row_id in 0..table.num_rows as usize {
1493                if let Some(val) = table.get_value(row_id, col_idx) {
1494                    if let Ok(vec) = crate::extract_f64_list_from_value(val) {
1495                        data.push((row_id, vec));
1496                    }
1497                }
1498            }
1499        }
1500        if data.is_empty() {
1501            if let Some(mut vi) = self.vector_indexes.get_mut(&index_id) {
1502                vi.hnsw_mut().clear();
1503            }
1504            return;
1505        }
1506
1507        let mut vi = self.vector_indexes.get_mut(&index_id);
1508        if let Some(vi) = vi.as_mut() {
1509            vi.hnsw_mut().clear();
1510            for (row_id, vec) in data {
1511                vi.hnsw_mut().insert(vec, row_id);
1512            }
1513        }
1514    }
1515
1516    /// Rebuild the vector indexes of all node tables written by a statement.
1517    ///
1518    /// Called after successful writes so the on-disk/in-memory HNSW graph never
1519    /// serves stale or wrongly-positioned rows (P52.38).
1520    pub fn refresh_vector_indexes_for_tables(&self, table_ids: &[u64]) {
1521        for table_id in table_ids {
1522            let table_name = match self.get_node_table(*table_id) {
1523                Some(t) => t.name.clone(),
1524                None => continue,
1525            };
1526            let index_ids: Vec<u64> = self
1527                .vector_indexes
1528                .iter()
1529                .filter(|vi| vi.table_name == table_name)
1530                .map(|vi| *vi.key())
1531                .collect();
1532            for index_id in index_ids {
1533                self.refresh_vector_index(index_id);
1534            }
1535        }
1536    }
1537
1538    /// Create an ART (Adaptive Radix Tree) index on a node table's PK column.
1539    ///
1540    /// Creates a new `ArtPrimaryKeyIndex`, backfills it with all existing rows,
1541    /// and attaches it to the `NodeTable`.
1542    ///
1543    /// The `index_name` is used as the BufferManager file name for persistence.
1544    pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
1545        let mut table = self
1546            .get_node_table_by_name_mut(table_name)
1547            .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1548
1549        if table.art_index.is_some() {
1550            return Err(StorageError::Index(format!(
1551                "Table '{table_name}' already has an ART index"
1552            )));
1553        }
1554
1555        let mut art_idx = ArtPrimaryKeyIndex::new(index_name);
1556
1557        // Backfill existing rows
1558        let pk_col = table.primary_key_column;
1559        // Scan all rows via to_column_major_data for backfill
1560        let col_major = table.to_column_major_data();
1561        if pk_col < col_major.len() {
1562            for (row_offset, pk_val) in col_major[pk_col].iter().enumerate() {
1563                if !matches!(pk_val, Value::Null)
1564                    && let Some(art_key) = ArtKey::from_value(pk_val)
1565                {
1566                    art_idx.insert(&art_key, row_offset as u64);
1567                }
1568            }
1569        }
1570
1571        table.art_index = Some(art_idx);
1572        Ok(())
1573    }
1574
1575    /// Drop the ART index from a node table.
1576    pub fn drop_art_index(&self, table_name: &str) -> Result<(), StorageError> {
1577        let mut table = self
1578            .get_node_table_by_name_mut(table_name)
1579            .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1580
1581        table.art_index = None;
1582        Ok(())
1583    }
1584
1585    /// Get a reference to the ART index for a node table (via table name).
1586    /// Returns `None` if the table has no ART index or doesn't exist.
1587    pub fn get_art_index(&self, table_name: &str) -> Option<ArtPrimaryKeyIndex> {
1588        let table = self.get_node_table_by_name(table_name)?;
1589        table.art_index.clone()
1590    }
1591
1592    /// Check if a node table has an ART index.
1593    pub fn has_art_index(&self, table_name: &str) -> bool {
1594        self.get_node_table_by_name(table_name)
1595            .map(|t| t.art_index.is_some())
1596            .unwrap_or(false)
1597    }
1598
1599    /// Remove a rel table by name. Returns true if the table existed.
1600    pub fn drop_rel_table(&self, name: &str) -> bool {
1601        if let Some(id) = self.rel_name_to_id.get(name) {
1602            let table_id = *id;
1603            drop(id);
1604            self.rel_name_to_id.remove(name);
1605            self.rel_tables.remove(&table_id).is_some()
1606        } else {
1607            false
1608        }
1609    }
1610}
1611
1612// ---------------------------------------------------------------------------
1613// Tests
1614// ---------------------------------------------------------------------------
1615
1616#[cfg(test)]
1617mod tests {
1618    use super::*;
1619    use crate::column_chunk::NODE_GROUP_SIZE;
1620    use std::sync::Arc;
1621
1622    // ==================== NodeTable tests ====================
1623
1624    #[test]
1625    fn test_node_table_empty() {
1626        let table = NodeTable::new(
1627            1,
1628            "Person".into(),
1629            vec![
1630                ColumnDefinition {
1631                    compression: akar_common::enums::CompressionType::Uncompressed,
1632                    name: "name".into(),
1633                    logical_type: LogicalTypeID::String,
1634                    is_primary_key: true,
1635                },
1636                ColumnDefinition {
1637                    compression: akar_common::enums::CompressionType::Uncompressed,
1638                    name: "age".into(),
1639                    logical_type: LogicalTypeID::Int64,
1640                    is_primary_key: false,
1641                },
1642            ],
1643        );
1644        assert_eq!(table.num_rows, 0);
1645        assert!(table.node_groups.is_empty());
1646    }
1647
1648    #[test]
1649    fn test_node_table_insert_and_get() {
1650        let mut table = NodeTable::new(
1651            1,
1652            "Person".into(),
1653            vec![
1654                ColumnDefinition {
1655                    compression: akar_common::enums::CompressionType::Uncompressed,
1656                    name: "name".into(),
1657                    logical_type: LogicalTypeID::String,
1658                    is_primary_key: true,
1659                },
1660                ColumnDefinition {
1661                    compression: akar_common::enums::CompressionType::Uncompressed,
1662                    name: "age".into(),
1663                    logical_type: LogicalTypeID::Int64,
1664                    is_primary_key: false,
1665                },
1666            ],
1667        );
1668        table
1669            .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1670            .unwrap();
1671        table
1672            .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1673            .unwrap();
1674
1675        assert_eq!(table.num_rows, 2);
1676        assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1677        assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1678    }
1679
1680    #[test]
1681    fn test_node_table_batch_insert_with_spiller_restores_all_rows() {
1682        let dir = tempfile::tempdir().unwrap();
1683        let spiller = Arc::new(crate::spiller::Spiller::new(dir.path(), 64));
1684        let mut table = NodeTable::new(
1685            1,
1686            "T".into(),
1687            vec![ColumnDefinition {
1688                compression: akar_common::enums::CompressionType::Uncompressed,
1689                name: "id".into(),
1690                logical_type: LogicalTypeID::Int64,
1691                is_primary_key: true,
1692            }],
1693        );
1694        table.set_spiller(Some(spiller));
1695
1696        let rows: Vec<Vec<Value>> = (0..2000).map(|i| vec![Value::Int64(i)]).collect();
1697        let inserted = table.insert_rows_batch(&rows).unwrap();
1698        assert_eq!(inserted, 2000);
1699        assert_eq!(table.num_rows, 2000);
1700
1701        for i in 0i64..2000 {
1702            assert_eq!(table.get_value(i as usize, 0), Some(&Value::Int64(i)));
1703        }
1704        assert!(
1705            table.node_groups.iter().all(|g| !g.has_spill_files()),
1706            "spill files must be merged back after the batch"
1707        );
1708    }
1709
1710    #[test]
1711    fn test_delete_row_removes_pk_from_hash_and_art_index() {
1712        let mut table = NodeTable::new(
1713            1,
1714            "Person".into(),
1715            vec![
1716                ColumnDefinition {
1717                    compression: akar_common::enums::CompressionType::Uncompressed,
1718                    name: "name".into(),
1719                    logical_type: LogicalTypeID::String,
1720                    is_primary_key: true,
1721                },
1722                ColumnDefinition {
1723                    compression: akar_common::enums::CompressionType::Uncompressed,
1724                    name: "age".into(),
1725                    logical_type: LogicalTypeID::Int64,
1726                    is_primary_key: false,
1727                },
1728            ],
1729        );
1730        table.art_index = Some(ArtPrimaryKeyIndex::new("test_art"));
1731        table
1732            .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1733            .unwrap();
1734        table
1735            .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1736            .unwrap();
1737        let art = table.art_index.as_ref().unwrap();
1738        assert_eq!(
1739            art.lookup(&ArtKey::from_value(&Value::String("Alice".into())).unwrap()),
1740            Some(0)
1741        );
1742
1743        table.delete_row(0).unwrap();
1744
1745        // Soft-deleted row must no longer resolve via PK lookup (P52.16).
1746        assert!(table.lookup_by_pk(&Value::String("Alice".into())).is_none());
1747        let art = table.art_index.as_ref().unwrap();
1748        assert_eq!(art.len(), 1, "ART must drop the deleted entry");
1749        assert!(
1750            art.lookup(&ArtKey::from_value(&Value::String("Alice".into())).unwrap())
1751                .is_none()
1752        );
1753        assert!(
1754            art.lookup(&ArtKey::from_value(&Value::String("Bob".into())).unwrap())
1755                .is_some()
1756        );
1757        // Range scan over the ART must not surface the deleted PK.
1758        let hits = table.lookup_by_pk_range(
1759            Some(&Value::String("A".into())),
1760            true,
1761            Some(&Value::String("C".into())),
1762            true,
1763            100,
1764        );
1765        assert_eq!(hits, vec![1], "only 'Bob' (row 1) should be in range");
1766
1767        // Re-inserting the same PK must now succeed (was: duplicate PK error).
1768        table
1769            .insert_row(vec![Value::String("Alice".into()), Value::Int64(31)])
1770            .unwrap();
1771        assert_eq!(table.lookup_by_pk(&Value::String("Alice".into())), Some(2));
1772    }
1773
1774    #[test]
1775    fn test_node_table_scan_column() {
1776        let mut table = NodeTable::new(
1777            1,
1778            "T".into(),
1779            vec![ColumnDefinition {
1780                compression: akar_common::enums::CompressionType::Uncompressed,
1781                name: "val".into(),
1782                logical_type: LogicalTypeID::Int64,
1783                is_primary_key: false,
1784            }],
1785        );
1786        for i in 0..100 {
1787            table.insert_row(vec![Value::Int64(i)]).unwrap();
1788        }
1789        let scanned = table.scan_column(0, 10, 5, None, &[]);
1790        assert_eq!(scanned.len(), 5);
1791        assert_eq!(scanned[0], Value::Int64(10));
1792        assert_eq!(scanned[4], Value::Int64(14));
1793    }
1794
1795    #[test]
1796    fn test_node_table_to_column_major() {
1797        let mut table = NodeTable::new(
1798            1,
1799            "T".into(),
1800            vec![
1801                ColumnDefinition {
1802                    compression: akar_common::enums::CompressionType::Uncompressed,
1803                    name: "x".into(),
1804                    logical_type: LogicalTypeID::Int64,
1805                    is_primary_key: false,
1806                },
1807                ColumnDefinition {
1808                    compression: akar_common::enums::CompressionType::Uncompressed,
1809                    name: "y".into(),
1810                    logical_type: LogicalTypeID::Int64,
1811                    is_primary_key: false,
1812                },
1813            ],
1814        );
1815        table.insert_row(vec![Value::Int64(1), Value::Int64(10)]).unwrap();
1816        table.insert_row(vec![Value::Int64(2), Value::Int64(20)]).unwrap();
1817
1818        let data = table.to_column_major_data();
1819        assert_eq!(data.len(), 2);
1820        assert_eq!(data[0], vec![Value::Int64(1), Value::Int64(2)]);
1821        assert_eq!(data[1], vec![Value::Int64(10), Value::Int64(20)]);
1822    }
1823
1824    #[test]
1825    fn test_node_table_auto_node_group() {
1826        let mut table = NodeTable::new(
1827            1,
1828            "T".into(),
1829            vec![ColumnDefinition {
1830                compression: akar_common::enums::CompressionType::Uncompressed,
1831                name: "v".into(),
1832                logical_type: LogicalTypeID::Int64,
1833                is_primary_key: false,
1834            }],
1835        );
1836        // Insert NODE_GROUP_SIZE + 1 rows to force a second node group
1837        for i in 0..NODE_GROUP_SIZE as u64 + 1 {
1838            table.insert_row(vec![Value::Int64(i as i64)]).unwrap();
1839        }
1840        assert_eq!(table.num_rows, NODE_GROUP_SIZE as u64 + 1);
1841        assert_eq!(table.node_groups.len(), 2);
1842        assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1843        assert_eq!(table.node_groups[1].num_nodes, 1);
1844        // Scan should still return all values
1845        assert_eq!(table.get_value(0, 0), Some(&Value::Int64(0)));
1846        assert_eq!(
1847            table.get_value(NODE_GROUP_SIZE, 0),
1848            Some(&Value::Int64(NODE_GROUP_SIZE as i64))
1849        );
1850    }
1851
1852    // ==================== RelTable (CSR) tests ====================
1853
1854    fn make_rel_table() -> RelTable {
1855        RelTable::new(
1856            1,
1857            "Knows".into(),
1858            0,
1859            1,
1860            vec![
1861                ColumnDefinition {
1862                    compression: akar_common::enums::CompressionType::Uncompressed,
1863                    name: "since".into(),
1864                    logical_type: LogicalTypeID::Int64,
1865                    is_primary_key: false,
1866                },
1867                ColumnDefinition {
1868                    compression: akar_common::enums::CompressionType::Uncompressed,
1869                    name: "weight".into(),
1870                    logical_type: LogicalTypeID::Double,
1871                    is_primary_key: false,
1872                },
1873            ],
1874        )
1875    }
1876
1877    #[test]
1878    fn test_rel_table_empty() {
1879        let rel = make_rel_table();
1880        assert_eq!(rel.num_rows, 0);
1881        assert!(rel.edges.is_empty());
1882        assert!(rel.fwd_adj.is_empty());
1883        assert!(rel.rev_adj.is_empty());
1884    }
1885
1886    #[test]
1887    fn test_rel_insert_basic() {
1888        let mut rel = make_rel_table();
1889        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1890            .unwrap();
1891        rel.insert_rel(0, 2, vec![Value::Int64(2021), Value::Double(0.8)])
1892            .unwrap();
1893        rel.insert_rel(1, 0, vec![Value::Int64(2020), Value::Double(0.3)])
1894            .unwrap();
1895
1896        assert_eq!(rel.num_rows, 3);
1897        assert_eq!(rel.edges.len(), 3);
1898
1899        // Forward adjacency from node 0
1900        let fwd = rel.scan_adj_list(0);
1901        assert_eq!(fwd.len(), 2);
1902        assert_eq!(fwd[0], (1, 0)); // (dst=1, edge_idx=0)
1903        assert_eq!(fwd[1], (2, 1)); // (dst=2, edge_idx=1)
1904
1905        // Forward from node 1
1906        let fwd1 = rel.scan_adj_list(1);
1907        assert_eq!(fwd1.len(), 1);
1908        assert_eq!(fwd1[0], (0, 2));
1909    }
1910
1911    #[test]
1912    fn test_rel_reverse_adjacency() {
1913        let mut rel = make_rel_table();
1914        rel.insert_rel(0, 5, vec![Value::Int64(2022), Value::Double(1.0)])
1915            .unwrap();
1916        rel.insert_rel(3, 5, vec![Value::Int64(2023), Value::Double(1.5)])
1917            .unwrap();
1918
1919        // Node 5 has two incoming edges
1920        let rev = rel.scan_rev_adj_list(5);
1921        assert_eq!(rev.len(), 2);
1922        assert_eq!(rev[0], (0, 0));
1923        assert_eq!(rev[1], (3, 1));
1924    }
1925
1926    #[test]
1927    fn test_rel_get_edge_properties() {
1928        let mut rel = make_rel_table();
1929        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1930            .unwrap();
1931        rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
1932            .unwrap();
1933
1934        let props0 = rel.get_edge_properties(0);
1935        assert_eq!(props0, vec![Value::Int64(2020), Value::Double(0.5)]);
1936
1937        let props1 = rel.get_edge_properties(1);
1938        assert_eq!(props1, vec![Value::Int64(2021), Value::Double(0.9)]);
1939    }
1940
1941    #[test]
1942    fn test_rel_get_outgoing_edges() {
1943        let mut rel = make_rel_table();
1944        rel.insert_rel(0, 10, vec![Value::Int64(2020), Value::Double(1.0)])
1945            .unwrap();
1946        rel.insert_rel(0, 20, vec![Value::Int64(2021), Value::Double(2.0)])
1947            .unwrap();
1948
1949        let outgoing = rel.get_outgoing_edges(0);
1950        assert_eq!(outgoing.len(), 2);
1951        assert_eq!(outgoing[0].0, 10);
1952        assert_eq!(outgoing[0].1, vec![Value::Int64(2020), Value::Double(1.0)]);
1953        assert_eq!(outgoing[1].0, 20);
1954    }
1955
1956    #[test]
1957    fn test_rel_get_incoming_edges() {
1958        let mut rel = make_rel_table();
1959        rel.insert_rel(10, 5, vec![Value::Int64(2020), Value::Double(1.0)])
1960            .unwrap();
1961        rel.insert_rel(20, 5, vec![Value::Int64(2021), Value::Double(2.0)])
1962            .unwrap();
1963
1964        let incoming = rel.get_incoming_edges(5);
1965        assert_eq!(incoming.len(), 2);
1966        assert_eq!(incoming[0].0, 10);
1967        assert_eq!(incoming[1].0, 20);
1968    }
1969
1970    #[test]
1971    fn test_rel_no_edges() {
1972        let rel = make_rel_table();
1973        assert!(rel.scan_adj_list(0).is_empty());
1974        assert!(rel.scan_rev_adj_list(0).is_empty());
1975        assert!(rel.get_outgoing_edges(0).is_empty());
1976        assert!(rel.get_incoming_edges(0).is_empty());
1977    }
1978
1979    #[test]
1980    fn test_rel_insert_row_legacy() {
1981        let mut rel = make_rel_table();
1982        // insert_row treats values as properties with sequential edge IDs
1983        rel.insert_row(vec![Value::Int64(2022), Value::Double(3.0)]).unwrap();
1984        assert_eq!(rel.num_rows, 1);
1985        assert_eq!(rel.edges[0], (0, 0)); // sequential from=0, to=0
1986        assert_eq!(rel.get_edge_properties(0), vec![Value::Int64(2022), Value::Double(3.0)]);
1987    }
1988
1989    #[test]
1990    fn test_rel_wrong_column_count() {
1991        let mut rel = make_rel_table();
1992        let result = rel.insert_rel(0, 1, vec![Value::Int64(42)]); // 1 value, expected 2
1993        assert!(result.is_err());
1994    }
1995
1996    #[test]
1997    fn test_rel_get_column() {
1998        let mut rel = make_rel_table();
1999        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(1.5)])
2000            .unwrap();
2001        rel.insert_rel(1, 2, vec![Value::Int64(2021), Value::Double(2.5)])
2002            .unwrap();
2003
2004        let since_col = rel.get_column(0).unwrap();
2005        assert_eq!(since_col, &[Value::Int64(2020), Value::Int64(2021)]);
2006
2007        let weight_col = rel.get_column(1).unwrap();
2008        assert_eq!(weight_col, &[Value::Double(1.5), Value::Double(2.5)]);
2009    }
2010
2011    #[test]
2012    fn test_rel_to_column_major() {
2013        let mut rel = make_rel_table();
2014        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
2015            .unwrap();
2016        rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
2017            .unwrap();
2018
2019        let data = rel.to_column_major_data();
2020        assert_eq!(data.len(), 2);
2021        assert_eq!(data[0], vec![Value::Int64(2020), Value::Int64(2021)]);
2022        assert_eq!(data[1], vec![Value::Double(0.5), Value::Double(0.9)]);
2023    }
2024
2025    // ==================== TableCatalog tests ====================
2026
2027    #[test]
2028    fn test_catalog_create_and_lookup() {
2029        let cat = TableCatalog::new();
2030        let node_table = cat.create_node_table(
2031            "Person".into(),
2032            vec![ColumnDefinition {
2033                compression: akar_common::enums::CompressionType::Uncompressed,
2034                name: "id".into(),
2035                logical_type: LogicalTypeID::Int64,
2036                is_primary_key: true,
2037            }],
2038        );
2039        assert_eq!(node_table.table_id, 0);
2040
2041        let rel_table = cat.create_rel_table(
2042            "Knows".into(),
2043            0,
2044            1,
2045            vec![ColumnDefinition {
2046                compression: akar_common::enums::CompressionType::Uncompressed,
2047                name: "since".into(),
2048                logical_type: LogicalTypeID::Int64,
2049                is_primary_key: false,
2050            }],
2051        );
2052        assert_eq!(rel_table.table_id, 1);
2053
2054        assert!(cat.get_node_table(0).is_some());
2055        assert!(cat.get_rel_table(1).is_some());
2056        assert_eq!(cat.node_table_num_rows("Person"), 0);
2057    }
2058
2059    #[test]
2060    fn test_refresh_vector_index_after_dml() {
2061        // P52.38: the HNSW graph used to be populated only during CREATE VECTOR
2062        // INDEX; rows inserted later went un-indexed. refresh_vector_indexes_for_tables
2063        // must rebuild the graph from the live table so post-index DML is visible.
2064        let cat = TableCatalog::new();
2065        cat.create_node_table(
2066            "Item".into(),
2067            vec![
2068                ColumnDefinition {
2069                    compression: akar_common::enums::CompressionType::Uncompressed,
2070                    name: "id".into(),
2071                    logical_type: LogicalTypeID::Int64,
2072                    is_primary_key: true,
2073                },
2074                ColumnDefinition {
2075                    compression: akar_common::enums::CompressionType::Uncompressed,
2076                    name: "embedding".into(),
2077                    logical_type: LogicalTypeID::List,
2078                    is_primary_key: false,
2079                },
2080            ],
2081        );
2082
2083        let vec3 = |x: f64, y: f64, z: f64| Value::List(vec![Value::Double(x), Value::Double(y), Value::Double(z)]);
2084        {
2085            let mut t = cat.get_node_table_by_name_mut("Item").unwrap();
2086            t.insert_row(vec![Value::Int64(1), vec3(1.0, 0.0, 0.0)]).unwrap();
2087            t.insert_row(vec![Value::Int64(2), vec3(0.0, 1.0, 0.0)]).unwrap();
2088            t.insert_row(vec![Value::Int64(3), vec3(0.0, 0.0, 1.0)]).unwrap();
2089        }
2090
2091        cat.create_vector_index(
2092            "item_vec".into(),
2093            "Item".into(),
2094            "embedding".into(),
2095            DistanceMetric::Cosine,
2096            3,
2097        );
2098
2099        // Seed the index from the 3 pre-existing rows; the vector for row 2 must exist.
2100        cat.refresh_vector_indexes_for_tables(&[0]);
2101        {
2102            let vi = cat.get_vector_index(1).unwrap();
2103            assert_eq!(vi.hnsw().len(), 3);
2104            assert!(vi.hnsw().get_vector(2).is_some());
2105        }
2106
2107        // Rows inserted AFTER the index was created must become searchable.
2108        {
2109            let mut t = cat.get_node_table_by_name_mut("Item").unwrap();
2110            t.insert_row(vec![Value::Int64(4), vec3(1.0, 1.0, 0.0)]).unwrap();
2111            t.insert_row(vec![Value::Int64(5), vec3(0.0, 1.0, 1.0)]).unwrap();
2112        }
2113
2114        cat.refresh_vector_indexes_for_tables(&[0]);
2115        let vi = cat.get_vector_index(1).unwrap();
2116        assert_eq!(vi.hnsw().len(), 5);
2117        assert!(vi.hnsw().get_vector(4).is_some());
2118    }
2119}