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