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(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    /// Filesystem root of the owning database, set once by
1242    /// [`crate::StorageManager::new`]. Extensions that keep side-car indexes on
1243    /// disk (e.g. the Tantivy FTS index under `<db_path>/fts/<index_name>`)
1244    /// read it here. `None` for in-memory (`:memory:`) or standalone catalogs.
1245    db_path: std::sync::RwLock<Option<std::path::PathBuf>>,
1246    /// Runtime handles for side-car indexes (currently the Tantivy FTS index,
1247    /// P107.2). Keyed by index name; values are type-erased
1248    /// `Arc<dyn Any + Send + Sync>` so this crate does not depend on the owning
1249    /// crate (`akar-fts`, whose dependency graph includes this one) — the
1250    /// owning crate downcasts on retrieval. This is how the commit-time sync
1251    /// hook and the read scans share ONE live reader (reloaded only at commit).
1252    /// Not persisted.
1253    fts_runtime_handles: DashMap<String, Arc<dyn std::any::Any + Send + Sync>>,
1254}
1255
1256impl std::fmt::Debug for TableCatalog {
1257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1258        f.debug_struct("TableCatalog")
1259            .field("node_tables", &self.node_tables)
1260            .field("rel_tables", &self.rel_tables)
1261            .field("vector_indexes", &self.vector_indexes)
1262            .field("node_name_to_id", &self.node_name_to_id)
1263            .field("rel_name_to_id", &self.rel_name_to_id)
1264            .field("vector_index_name_to_id", &self.vector_index_name_to_id)
1265            .field("next_table_id", &self.next_table_id)
1266            .field("db_path", &self.db_path)
1267            .field("fts_runtime_handles", &self.fts_runtime_handles.len())
1268            .finish()
1269    }
1270}
1271
1272impl TableCatalog {
1273    pub fn new() -> Self {
1274        Self::default()
1275    }
1276
1277    /// Record the database's filesystem root (called by
1278    /// [`crate::StorageManager::new`]). Last write wins.
1279    pub fn set_db_path(&self, path: std::path::PathBuf) {
1280        if let Ok(mut guard) = self.db_path.write() {
1281            *guard = Some(path);
1282        }
1283    }
1284
1285    /// The database's filesystem root, if known. `None` for in-memory or
1286    /// standalone catalogs.
1287    pub fn db_path(&self) -> Option<std::path::PathBuf> {
1288        self.db_path.read().ok().and_then(|guard| guard.clone())
1289    }
1290
1291    /// Get the runtime handle registered for side-car index `name` (FTS,
1292    /// P107.2), if any. The value is type-erased; the owning crate downcasts it
1293    /// via [`std::any::Any::downcast_arc`].
1294    pub fn fts_runtime_handle(&self, name: &str) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
1295        self.fts_runtime_handles.get(name).map(|guard| guard.clone())
1296    }
1297
1298    /// Register (or replace) the runtime handle for side-car index `name`.
1299    ///
1300    /// Used by the FTS extension to make the commit-time sync hook and the read
1301    /// scans share ONE live `IndexReader` (reloaded only at commit, P107.2).
1302    pub fn set_fts_runtime_handle(&self, name: &str, handle: Arc<dyn std::any::Any + Send + Sync>) {
1303        self.fts_runtime_handles.insert(name.to_string(), handle);
1304    }
1305
1306    pub fn create_node_table(&self, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1307        let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1308        let table = NodeTable::new(table_id, name.clone(), columns);
1309        self.node_name_to_id.insert(name, table_id);
1310        self.node_tables.insert(table_id, table.clone());
1311        table
1312    }
1313
1314    /// Recreate a node table at a specific table ID (used when restoring a
1315    /// persisted catalog during recovery). Advances `next_table_id` so that
1316    /// subsequent auto-assigned IDs never collide with restored ones.
1317    pub fn create_node_table_with_id(&self, table_id: u64, name: String, columns: Vec<ColumnDefinition>) -> NodeTable {
1318        self.bump_next_table_id(table_id);
1319        let table = NodeTable::new(table_id, name.clone(), columns);
1320        self.node_name_to_id.insert(name, table_id);
1321        self.node_tables.insert(table_id, table.clone());
1322        table
1323    }
1324
1325    pub fn create_rel_table(
1326        &self,
1327        name: String,
1328        src_table_id: u64,
1329        dst_table_id: u64,
1330        columns: Vec<ColumnDefinition>,
1331    ) -> RelTable {
1332        let table_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1333        let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1334        self.rel_name_to_id.insert(name, table_id);
1335        self.rel_tables.insert(table_id, table.clone());
1336        table
1337    }
1338
1339    /// Recreate a rel table at a specific table ID (used when restoring a
1340    /// persisted catalog during recovery). Advances `next_table_id` so that
1341    /// subsequent auto-assigned IDs never collide with restored ones.
1342    pub fn create_rel_table_with_id(
1343        &self,
1344        table_id: u64,
1345        name: String,
1346        src_table_id: u64,
1347        dst_table_id: u64,
1348        columns: Vec<ColumnDefinition>,
1349    ) -> RelTable {
1350        self.bump_next_table_id(table_id);
1351        let table = RelTable::new(table_id, name.clone(), src_table_id, dst_table_id, columns);
1352        self.rel_name_to_id.insert(name, table_id);
1353        self.rel_tables.insert(table_id, table.clone());
1354        table
1355    }
1356
1357    /// Advance `next_table_id` to be strictly greater than `table_id` so
1358    /// restored IDs are never re-issued by `create_node_table`/`create_rel_table`.
1359    fn bump_next_table_id(&self, table_id: u64) {
1360        let mut next = self.next_table_id.load(std::sync::atomic::Ordering::SeqCst);
1361        while next <= table_id {
1362            match self.next_table_id.compare_exchange(
1363                next,
1364                table_id + 1,
1365                std::sync::atomic::Ordering::SeqCst,
1366                std::sync::atomic::Ordering::SeqCst,
1367            ) {
1368                Ok(_) => break,
1369                Err(current) => next = current,
1370            }
1371        }
1372    }
1373
1374    pub fn get_node_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1375        self.node_tables.get(&table_id)
1376    }
1377
1378    pub fn get_node_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1379        self.node_tables.get_mut(&table_id)
1380    }
1381
1382    pub fn get_node_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, NodeTable>> {
1383        let id = self.node_name_to_id.get(name)?;
1384        self.node_tables.get(&*id)
1385    }
1386
1387    pub fn get_node_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, NodeTable>> {
1388        let id = self.node_name_to_id.get(name)?;
1389        self.node_tables.get_mut(&*id)
1390    }
1391
1392    pub fn get_rel_table(&self, table_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1393        self.rel_tables.get(&table_id)
1394    }
1395
1396    pub fn get_rel_table_mut(&self, table_id: u64) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1397        self.rel_tables.get_mut(&table_id)
1398    }
1399
1400    pub fn get_rel_table_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, RelTable>> {
1401        let id = self.rel_name_to_id.get(name)?;
1402        self.rel_tables.get(&*id)
1403    }
1404
1405    pub fn get_rel_table_by_name_mut(&self, name: &str) -> Option<dashmap::mapref::one::RefMut<'_, u64, RelTable>> {
1406        let id = self.rel_name_to_id.get(name)?;
1407        self.rel_tables.get_mut(&*id)
1408    }
1409
1410    /// Check if a node has any incident edges.
1411    pub fn has_incident_edges(&self, table_id: u64, node_idx: u64) -> bool {
1412        for rel_table in self.rel_tables.iter() {
1413            if rel_table.src_table_id == table_id {
1414                if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1415                    if !edges.is_empty() {
1416                        return true;
1417                    }
1418                }
1419            }
1420            if rel_table.dst_table_id == table_id {
1421                if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1422                    if !edges.is_empty() {
1423                        return true;
1424                    }
1425                }
1426            }
1427        }
1428        false
1429    }
1430
1431    /// Delete all incident edges for a given node.
1432    pub fn detach_node(&self, table_id: u64, node_idx: u64) {
1433        for mut rel_table in self.rel_tables.iter_mut() {
1434            let mut edges_to_delete = Vec::new();
1435
1436            if rel_table.src_table_id == table_id {
1437                if let Some(edges) = rel_table.fwd_adj.get(&node_idx) {
1438                    for &(_, edge_idx) in edges {
1439                        edges_to_delete.push(edge_idx);
1440                    }
1441                }
1442            }
1443
1444            if rel_table.dst_table_id == table_id {
1445                if let Some(edges) = rel_table.rev_adj.get(&node_idx) {
1446                    for &(_, edge_idx) in edges {
1447                        edges_to_delete.push(edge_idx);
1448                    }
1449                }
1450            }
1451
1452            for edge_idx in edges_to_delete {
1453                let _ = rel_table.delete_edge(edge_idx);
1454            }
1455        }
1456    }
1457
1458    pub fn all_node_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, NodeTable>> {
1459        self.node_tables.iter().collect()
1460    }
1461
1462    pub fn all_rel_tables(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, RelTable>> {
1463        self.rel_tables.iter().collect()
1464    }
1465
1466    /// Get the number of rows in a node table by name.
1467    pub fn node_table_num_rows(&self, name: &str) -> u64 {
1468        self.get_node_table_by_name(name).map(|t| t.num_rows).unwrap_or(0)
1469    }
1470
1471    /// Remove a node table by name. Returns true if the table existed.
1472    pub fn drop_node_table(&self, name: &str) -> bool {
1473        if let Some(id) = self.node_name_to_id.get(name) {
1474            let table_id = *id;
1475            drop(id);
1476            self.node_name_to_id.remove(name);
1477            self.node_tables.remove(&table_id).is_some()
1478        } else {
1479            false
1480        }
1481    }
1482
1483    /// Create a new vector index in the catalog.
1484    pub fn create_vector_index(
1485        &self,
1486        name: String,
1487        table_name: String,
1488        column_name: String,
1489        metric: DistanceMetric,
1490        dimensions: u32,
1491    ) -> VectorIndexTable {
1492        let index_id = self.next_table_id.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1493        let table = VectorIndexTable::new(index_id, name.clone(), table_name, column_name, metric, dimensions);
1494        self.vector_index_name_to_id.insert(name, index_id);
1495        self.vector_indexes.insert(index_id, table.clone());
1496        table
1497    }
1498
1499    /// Get a vector index by its ID.
1500    pub fn get_vector_index(&self, index_id: u64) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1501        self.vector_indexes.get(&index_id)
1502    }
1503
1504    /// Get a vector index by name.
1505    pub fn get_vector_index_by_name(&self, name: &str) -> Option<dashmap::mapref::one::Ref<'_, u64, VectorIndexTable>> {
1506        let id = self.vector_index_name_to_id.get(name)?;
1507        self.vector_indexes.get(&*id)
1508    }
1509
1510    /// Get a mutable vector index by name.
1511    pub fn get_vector_index_by_name_mut(
1512        &self,
1513        name: &str,
1514    ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1515        let id = self.vector_index_name_to_id.get(name)?;
1516        self.vector_indexes.get_mut(&*id)
1517    }
1518
1519    /// Get a mutable vector index by ID.
1520    pub fn get_vector_index_mut(
1521        &self,
1522        index_id: u64,
1523    ) -> Option<dashmap::mapref::one::RefMut<'_, u64, VectorIndexTable>> {
1524        self.vector_indexes.get_mut(&index_id)
1525    }
1526
1527    /// Remove a vector index by name. Returns true if the index existed.
1528    pub fn drop_vector_index(&self, name: &str) -> bool {
1529        if let Some(id) = self.vector_index_name_to_id.get(name) {
1530            let index_id = *id;
1531            drop(id);
1532            self.vector_index_name_to_id.remove(name);
1533            self.vector_indexes.remove(&index_id).is_some()
1534        } else {
1535            false
1536        }
1537    }
1538
1539    /// Get all vector indexes.
1540    pub fn all_vector_indexes(&self) -> Vec<dashmap::mapref::multiple::RefMulti<'_, u64, VectorIndexTable>> {
1541        self.vector_indexes.iter().collect()
1542    }
1543
1544    /// Rebuild a vector index from the current contents of its node table.
1545    ///
1546    /// The HNSW graph is re-populated with the live row ids and vectors, so
1547    /// INSERT/DELETE after `CREATE VECTOR INDEX` are always reflected (P52.38).
1548    pub fn refresh_vector_index(&self, index_id: u64) {
1549        let (table_name, column_name) = match self.vector_indexes.get(&index_id) {
1550            Some(vi) => (vi.table_name.clone(), vi.column_name.clone()),
1551            None => return,
1552        };
1553
1554        let col_idx = match self.get_node_table_by_name(&table_name) {
1555            Some(t) => match t.columns.iter().position(|c| c.name == column_name) {
1556                Some(idx) => idx,
1557                None => return,
1558            },
1559            None => return,
1560        };
1561
1562        // Collect (row_id, vector) pairs while holding the read reference,
1563        // then re-insert under a single mutable borrow of the index.
1564        let mut data: Vec<(usize, Vec<f64>)> = Vec::new();
1565        if let Some(table) = self.get_node_table_by_name(&table_name) {
1566            for row_id in 0..table.num_rows as usize {
1567                if let Some(val) = table.get_value(row_id, col_idx) {
1568                    if let Ok(vec) = crate::extract_f64_list_from_value(val) {
1569                        data.push((row_id, vec));
1570                    }
1571                }
1572            }
1573        }
1574        if data.is_empty() {
1575            if let Some(mut vi) = self.vector_indexes.get_mut(&index_id) {
1576                vi.hnsw_mut().clear();
1577            }
1578            return;
1579        }
1580
1581        let mut vi = self.vector_indexes.get_mut(&index_id);
1582        if let Some(vi) = vi.as_mut() {
1583            vi.hnsw_mut().clear();
1584            for (row_id, vec) in data {
1585                vi.hnsw_mut().insert(vec, row_id);
1586            }
1587        }
1588    }
1589
1590    /// Rebuild the vector indexes of all node tables written by a statement.
1591    ///
1592    /// Called after successful writes so the on-disk/in-memory HNSW graph never
1593    /// serves stale or wrongly-positioned rows (P52.38).
1594    pub fn refresh_vector_indexes_for_tables(&self, table_ids: &[u64]) {
1595        for table_id in table_ids {
1596            let table_name = match self.get_node_table(*table_id) {
1597                Some(t) => t.name.clone(),
1598                None => continue,
1599            };
1600            let index_ids: Vec<u64> = self
1601                .vector_indexes
1602                .iter()
1603                .filter(|vi| vi.table_name == table_name)
1604                .map(|vi| *vi.key())
1605                .collect();
1606            for index_id in index_ids {
1607                self.refresh_vector_index(index_id);
1608            }
1609        }
1610    }
1611
1612    /// Create an ART (Adaptive Radix Tree) index on a node table's PK column.
1613    ///
1614    /// Creates a new `ArtPrimaryKeyIndex`, backfills it with all existing rows,
1615    /// and attaches it to the `NodeTable`.
1616    ///
1617    /// The `index_name` is used as the BufferManager file name for persistence.
1618    pub fn create_art_index(&self, table_name: &str, index_name: &str) -> Result<(), StorageError> {
1619        let mut table = self
1620            .get_node_table_by_name_mut(table_name)
1621            .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1622
1623        if table.art_index.is_some() {
1624            return Err(StorageError::Index(format!(
1625                "Table '{table_name}' already has an ART index"
1626            )));
1627        }
1628
1629        let mut art_idx = ArtPrimaryKeyIndex::new(index_name);
1630
1631        // Backfill existing rows
1632        let pk_col = table.primary_key_column;
1633        // Scan all rows via to_column_major_data for backfill
1634        let col_major = table.to_column_major_data();
1635        if pk_col < col_major.len() {
1636            for (row_offset, pk_val) in col_major[pk_col].iter().enumerate() {
1637                if !matches!(pk_val, Value::Null)
1638                    && let Some(art_key) = ArtKey::from_value(pk_val)
1639                {
1640                    art_idx.insert(&art_key, row_offset as u64);
1641                }
1642            }
1643        }
1644
1645        table.art_index = Some(art_idx);
1646        Ok(())
1647    }
1648
1649    /// Drop the ART index from a node table.
1650    pub fn drop_art_index(&self, table_name: &str) -> Result<(), StorageError> {
1651        let mut table = self
1652            .get_node_table_by_name_mut(table_name)
1653            .ok_or_else(|| StorageError::TableNotFound(format!("Node table '{table_name}' not found")))?;
1654
1655        table.art_index = None;
1656        Ok(())
1657    }
1658
1659    /// Get a reference to the ART index for a node table (via table name).
1660    /// Returns `None` if the table has no ART index or doesn't exist.
1661    pub fn get_art_index(&self, table_name: &str) -> Option<ArtPrimaryKeyIndex> {
1662        let table = self.get_node_table_by_name(table_name)?;
1663        table.art_index.clone()
1664    }
1665
1666    /// Check if a node table has an ART index.
1667    pub fn has_art_index(&self, table_name: &str) -> bool {
1668        self.get_node_table_by_name(table_name)
1669            .map(|t| t.art_index.is_some())
1670            .unwrap_or(false)
1671    }
1672
1673    /// Remove a rel table by name. Returns true if the table existed.
1674    pub fn drop_rel_table(&self, name: &str) -> bool {
1675        if let Some(id) = self.rel_name_to_id.get(name) {
1676            let table_id = *id;
1677            drop(id);
1678            self.rel_name_to_id.remove(name);
1679            self.rel_tables.remove(&table_id).is_some()
1680        } else {
1681            false
1682        }
1683    }
1684}
1685
1686// ---------------------------------------------------------------------------
1687// Tests
1688// ---------------------------------------------------------------------------
1689
1690#[cfg(test)]
1691mod tests {
1692    use super::*;
1693    use crate::column_chunk::NODE_GROUP_SIZE;
1694    use std::sync::Arc;
1695
1696    // ==================== NodeTable tests ====================
1697
1698    #[test]
1699    fn test_node_table_empty() {
1700        let table = NodeTable::new(
1701            1,
1702            "Person".into(),
1703            vec![
1704                ColumnDefinition {
1705                    compression: akar_common::enums::CompressionType::Uncompressed,
1706                    name: "name".into(),
1707                    logical_type: LogicalTypeID::String,
1708                    is_primary_key: true,
1709                },
1710                ColumnDefinition {
1711                    compression: akar_common::enums::CompressionType::Uncompressed,
1712                    name: "age".into(),
1713                    logical_type: LogicalTypeID::Int64,
1714                    is_primary_key: false,
1715                },
1716            ],
1717        );
1718        assert_eq!(table.num_rows, 0);
1719        assert!(table.node_groups.is_empty());
1720    }
1721
1722    #[test]
1723    fn test_node_table_insert_and_get() {
1724        let mut table = NodeTable::new(
1725            1,
1726            "Person".into(),
1727            vec![
1728                ColumnDefinition {
1729                    compression: akar_common::enums::CompressionType::Uncompressed,
1730                    name: "name".into(),
1731                    logical_type: LogicalTypeID::String,
1732                    is_primary_key: true,
1733                },
1734                ColumnDefinition {
1735                    compression: akar_common::enums::CompressionType::Uncompressed,
1736                    name: "age".into(),
1737                    logical_type: LogicalTypeID::Int64,
1738                    is_primary_key: false,
1739                },
1740            ],
1741        );
1742        table
1743            .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1744            .unwrap();
1745        table
1746            .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1747            .unwrap();
1748
1749        assert_eq!(table.num_rows, 2);
1750        assert_eq!(table.get_value(0, 0), Some(&Value::String("Alice".into())));
1751        assert_eq!(table.get_value(1, 1), Some(&Value::Int64(25)));
1752    }
1753
1754    #[test]
1755    fn test_node_table_batch_insert_with_spiller_restores_all_rows() {
1756        let dir = tempfile::tempdir().unwrap();
1757        let spiller = Arc::new(crate::spiller::Spiller::new(dir.path(), 64));
1758        let mut table = NodeTable::new(
1759            1,
1760            "T".into(),
1761            vec![ColumnDefinition {
1762                compression: akar_common::enums::CompressionType::Uncompressed,
1763                name: "id".into(),
1764                logical_type: LogicalTypeID::Int64,
1765                is_primary_key: true,
1766            }],
1767        );
1768        table.set_spiller(Some(spiller));
1769
1770        let rows: Vec<Vec<Value>> = (0..2000).map(|i| vec![Value::Int64(i)]).collect();
1771        let inserted = table.insert_rows_batch(&rows).unwrap();
1772        assert_eq!(inserted, 2000);
1773        assert_eq!(table.num_rows, 2000);
1774
1775        for i in 0i64..2000 {
1776            assert_eq!(table.get_value(i as usize, 0), Some(&Value::Int64(i)));
1777        }
1778        assert!(
1779            table.node_groups.iter().all(|g| !g.has_spill_files()),
1780            "spill files must be merged back after the batch"
1781        );
1782    }
1783
1784    #[test]
1785    fn test_delete_row_removes_pk_from_hash_and_art_index() {
1786        let mut table = NodeTable::new(
1787            1,
1788            "Person".into(),
1789            vec![
1790                ColumnDefinition {
1791                    compression: akar_common::enums::CompressionType::Uncompressed,
1792                    name: "name".into(),
1793                    logical_type: LogicalTypeID::String,
1794                    is_primary_key: true,
1795                },
1796                ColumnDefinition {
1797                    compression: akar_common::enums::CompressionType::Uncompressed,
1798                    name: "age".into(),
1799                    logical_type: LogicalTypeID::Int64,
1800                    is_primary_key: false,
1801                },
1802            ],
1803        );
1804        table.art_index = Some(ArtPrimaryKeyIndex::new("test_art"));
1805        table
1806            .insert_row(vec![Value::String("Alice".into()), Value::Int64(30)])
1807            .unwrap();
1808        table
1809            .insert_row(vec![Value::String("Bob".into()), Value::Int64(25)])
1810            .unwrap();
1811        let art = table.art_index.as_ref().unwrap();
1812        assert_eq!(
1813            art.lookup(&ArtKey::from_value(&Value::String("Alice".into())).unwrap()),
1814            Some(0)
1815        );
1816
1817        table.delete_row(0).unwrap();
1818
1819        // Soft-deleted row must no longer resolve via PK lookup (P52.16).
1820        assert!(table.lookup_by_pk(&Value::String("Alice".into())).is_none());
1821        let art = table.art_index.as_ref().unwrap();
1822        assert_eq!(art.len(), 1, "ART must drop the deleted entry");
1823        assert!(
1824            art.lookup(&ArtKey::from_value(&Value::String("Alice".into())).unwrap())
1825                .is_none()
1826        );
1827        assert!(
1828            art.lookup(&ArtKey::from_value(&Value::String("Bob".into())).unwrap())
1829                .is_some()
1830        );
1831        // Range scan over the ART must not surface the deleted PK.
1832        let hits = table.lookup_by_pk_range(
1833            Some(&Value::String("A".into())),
1834            true,
1835            Some(&Value::String("C".into())),
1836            true,
1837            100,
1838        );
1839        assert_eq!(hits, vec![1], "only 'Bob' (row 1) should be in range");
1840
1841        // Re-inserting the same PK must now succeed (was: duplicate PK error).
1842        table
1843            .insert_row(vec![Value::String("Alice".into()), Value::Int64(31)])
1844            .unwrap();
1845        assert_eq!(table.lookup_by_pk(&Value::String("Alice".into())), Some(2));
1846    }
1847
1848    #[test]
1849    fn test_node_table_scan_column() {
1850        let mut table = NodeTable::new(
1851            1,
1852            "T".into(),
1853            vec![ColumnDefinition {
1854                compression: akar_common::enums::CompressionType::Uncompressed,
1855                name: "val".into(),
1856                logical_type: LogicalTypeID::Int64,
1857                is_primary_key: false,
1858            }],
1859        );
1860        for i in 0..100 {
1861            table.insert_row(vec![Value::Int64(i)]).unwrap();
1862        }
1863        let scanned = table.scan_column(0, 10, 5, None, &HashMap::new());
1864        assert_eq!(scanned.len(), 5);
1865        assert_eq!(scanned[0], Value::Int64(10));
1866        assert_eq!(scanned[4], Value::Int64(14));
1867    }
1868
1869    #[test]
1870    fn test_node_table_to_column_major() {
1871        let mut table = NodeTable::new(
1872            1,
1873            "T".into(),
1874            vec![
1875                ColumnDefinition {
1876                    compression: akar_common::enums::CompressionType::Uncompressed,
1877                    name: "x".into(),
1878                    logical_type: LogicalTypeID::Int64,
1879                    is_primary_key: false,
1880                },
1881                ColumnDefinition {
1882                    compression: akar_common::enums::CompressionType::Uncompressed,
1883                    name: "y".into(),
1884                    logical_type: LogicalTypeID::Int64,
1885                    is_primary_key: false,
1886                },
1887            ],
1888        );
1889        table.insert_row(vec![Value::Int64(1), Value::Int64(10)]).unwrap();
1890        table.insert_row(vec![Value::Int64(2), Value::Int64(20)]).unwrap();
1891
1892        let data = table.to_column_major_data();
1893        assert_eq!(data.len(), 2);
1894        assert_eq!(data[0], vec![Value::Int64(1), Value::Int64(2)]);
1895        assert_eq!(data[1], vec![Value::Int64(10), Value::Int64(20)]);
1896    }
1897
1898    #[test]
1899    fn test_node_table_auto_node_group() {
1900        let mut table = NodeTable::new(
1901            1,
1902            "T".into(),
1903            vec![ColumnDefinition {
1904                compression: akar_common::enums::CompressionType::Uncompressed,
1905                name: "v".into(),
1906                logical_type: LogicalTypeID::Int64,
1907                is_primary_key: false,
1908            }],
1909        );
1910        // Insert NODE_GROUP_SIZE + 1 rows to force a second node group
1911        for i in 0..NODE_GROUP_SIZE as u64 + 1 {
1912            table.insert_row(vec![Value::Int64(i as i64)]).unwrap();
1913        }
1914        assert_eq!(table.num_rows, NODE_GROUP_SIZE as u64 + 1);
1915        assert_eq!(table.node_groups.len(), 2);
1916        assert_eq!(table.node_groups[0].num_nodes, NODE_GROUP_SIZE as u64);
1917        assert_eq!(table.node_groups[1].num_nodes, 1);
1918        // Scan should still return all values
1919        assert_eq!(table.get_value(0, 0), Some(&Value::Int64(0)));
1920        assert_eq!(
1921            table.get_value(NODE_GROUP_SIZE, 0),
1922            Some(&Value::Int64(NODE_GROUP_SIZE as i64))
1923        );
1924    }
1925
1926    // ==================== RelTable (CSR) tests ====================
1927
1928    fn make_rel_table() -> RelTable {
1929        RelTable::new(
1930            1,
1931            "Knows".into(),
1932            0,
1933            1,
1934            vec![
1935                ColumnDefinition {
1936                    compression: akar_common::enums::CompressionType::Uncompressed,
1937                    name: "since".into(),
1938                    logical_type: LogicalTypeID::Int64,
1939                    is_primary_key: false,
1940                },
1941                ColumnDefinition {
1942                    compression: akar_common::enums::CompressionType::Uncompressed,
1943                    name: "weight".into(),
1944                    logical_type: LogicalTypeID::Double,
1945                    is_primary_key: false,
1946                },
1947            ],
1948        )
1949    }
1950
1951    #[test]
1952    fn test_rel_table_empty() {
1953        let rel = make_rel_table();
1954        assert_eq!(rel.num_rows, 0);
1955        assert!(rel.edges.is_empty());
1956        assert!(rel.fwd_adj.is_empty());
1957        assert!(rel.rev_adj.is_empty());
1958    }
1959
1960    #[test]
1961    fn test_rel_insert_basic() {
1962        let mut rel = make_rel_table();
1963        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
1964            .unwrap();
1965        rel.insert_rel(0, 2, vec![Value::Int64(2021), Value::Double(0.8)])
1966            .unwrap();
1967        rel.insert_rel(1, 0, vec![Value::Int64(2020), Value::Double(0.3)])
1968            .unwrap();
1969
1970        assert_eq!(rel.num_rows, 3);
1971        assert_eq!(rel.edges.len(), 3);
1972
1973        // Forward adjacency from node 0
1974        let fwd = rel.scan_adj_list(0);
1975        assert_eq!(fwd.len(), 2);
1976        assert_eq!(fwd[0], (1, 0)); // (dst=1, edge_idx=0)
1977        assert_eq!(fwd[1], (2, 1)); // (dst=2, edge_idx=1)
1978
1979        // Forward from node 1
1980        let fwd1 = rel.scan_adj_list(1);
1981        assert_eq!(fwd1.len(), 1);
1982        assert_eq!(fwd1[0], (0, 2));
1983    }
1984
1985    #[test]
1986    fn test_rel_reverse_adjacency() {
1987        let mut rel = make_rel_table();
1988        rel.insert_rel(0, 5, vec![Value::Int64(2022), Value::Double(1.0)])
1989            .unwrap();
1990        rel.insert_rel(3, 5, vec![Value::Int64(2023), Value::Double(1.5)])
1991            .unwrap();
1992
1993        // Node 5 has two incoming edges
1994        let rev = rel.scan_rev_adj_list(5);
1995        assert_eq!(rev.len(), 2);
1996        assert_eq!(rev[0], (0, 0));
1997        assert_eq!(rev[1], (3, 1));
1998    }
1999
2000    #[test]
2001    fn test_rel_get_edge_properties() {
2002        let mut rel = make_rel_table();
2003        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
2004            .unwrap();
2005        rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
2006            .unwrap();
2007
2008        let props0 = rel.get_edge_properties(0);
2009        assert_eq!(props0, vec![Value::Int64(2020), Value::Double(0.5)]);
2010
2011        let props1 = rel.get_edge_properties(1);
2012        assert_eq!(props1, vec![Value::Int64(2021), Value::Double(0.9)]);
2013    }
2014
2015    #[test]
2016    fn test_rel_get_outgoing_edges() {
2017        let mut rel = make_rel_table();
2018        rel.insert_rel(0, 10, vec![Value::Int64(2020), Value::Double(1.0)])
2019            .unwrap();
2020        rel.insert_rel(0, 20, vec![Value::Int64(2021), Value::Double(2.0)])
2021            .unwrap();
2022
2023        let outgoing = rel.get_outgoing_edges(0);
2024        assert_eq!(outgoing.len(), 2);
2025        assert_eq!(outgoing[0].0, 10);
2026        assert_eq!(outgoing[0].1, vec![Value::Int64(2020), Value::Double(1.0)]);
2027        assert_eq!(outgoing[1].0, 20);
2028    }
2029
2030    #[test]
2031    fn test_rel_get_incoming_edges() {
2032        let mut rel = make_rel_table();
2033        rel.insert_rel(10, 5, vec![Value::Int64(2020), Value::Double(1.0)])
2034            .unwrap();
2035        rel.insert_rel(20, 5, vec![Value::Int64(2021), Value::Double(2.0)])
2036            .unwrap();
2037
2038        let incoming = rel.get_incoming_edges(5);
2039        assert_eq!(incoming.len(), 2);
2040        assert_eq!(incoming[0].0, 10);
2041        assert_eq!(incoming[1].0, 20);
2042    }
2043
2044    #[test]
2045    fn test_rel_no_edges() {
2046        let rel = make_rel_table();
2047        assert!(rel.scan_adj_list(0).is_empty());
2048        assert!(rel.scan_rev_adj_list(0).is_empty());
2049        assert!(rel.get_outgoing_edges(0).is_empty());
2050        assert!(rel.get_incoming_edges(0).is_empty());
2051    }
2052
2053    #[test]
2054    fn test_rel_insert_row_legacy() {
2055        let mut rel = make_rel_table();
2056        // insert_row treats values as properties with sequential edge IDs
2057        rel.insert_row(vec![Value::Int64(2022), Value::Double(3.0)]).unwrap();
2058        assert_eq!(rel.num_rows, 1);
2059        assert_eq!(rel.edges[0], (0, 0)); // sequential from=0, to=0
2060        assert_eq!(rel.get_edge_properties(0), vec![Value::Int64(2022), Value::Double(3.0)]);
2061    }
2062
2063    #[test]
2064    fn test_rel_wrong_column_count() {
2065        let mut rel = make_rel_table();
2066        let result = rel.insert_rel(0, 1, vec![Value::Int64(42)]); // 1 value, expected 2
2067        assert!(result.is_err());
2068    }
2069
2070    #[test]
2071    fn test_rel_get_column() {
2072        let mut rel = make_rel_table();
2073        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(1.5)])
2074            .unwrap();
2075        rel.insert_rel(1, 2, vec![Value::Int64(2021), Value::Double(2.5)])
2076            .unwrap();
2077
2078        let since_col = rel.get_column(0).unwrap();
2079        assert_eq!(since_col, &[Value::Int64(2020), Value::Int64(2021)]);
2080
2081        let weight_col = rel.get_column(1).unwrap();
2082        assert_eq!(weight_col, &[Value::Double(1.5), Value::Double(2.5)]);
2083    }
2084
2085    #[test]
2086    fn test_rel_to_column_major() {
2087        let mut rel = make_rel_table();
2088        rel.insert_rel(0, 1, vec![Value::Int64(2020), Value::Double(0.5)])
2089            .unwrap();
2090        rel.insert_rel(2, 3, vec![Value::Int64(2021), Value::Double(0.9)])
2091            .unwrap();
2092
2093        let data = rel.to_column_major_data();
2094        assert_eq!(data.len(), 2);
2095        assert_eq!(data[0], vec![Value::Int64(2020), Value::Int64(2021)]);
2096        assert_eq!(data[1], vec![Value::Double(0.5), Value::Double(0.9)]);
2097    }
2098
2099    // ==================== TableCatalog tests ====================
2100
2101    #[test]
2102    fn test_catalog_create_and_lookup() {
2103        let cat = TableCatalog::new();
2104        let node_table = cat.create_node_table(
2105            "Person".into(),
2106            vec![ColumnDefinition {
2107                compression: akar_common::enums::CompressionType::Uncompressed,
2108                name: "id".into(),
2109                logical_type: LogicalTypeID::Int64,
2110                is_primary_key: true,
2111            }],
2112        );
2113        assert_eq!(node_table.table_id, 0);
2114
2115        let rel_table = cat.create_rel_table(
2116            "Knows".into(),
2117            0,
2118            1,
2119            vec![ColumnDefinition {
2120                compression: akar_common::enums::CompressionType::Uncompressed,
2121                name: "since".into(),
2122                logical_type: LogicalTypeID::Int64,
2123                is_primary_key: false,
2124            }],
2125        );
2126        assert_eq!(rel_table.table_id, 1);
2127
2128        assert!(cat.get_node_table(0).is_some());
2129        assert!(cat.get_rel_table(1).is_some());
2130        assert_eq!(cat.node_table_num_rows("Person"), 0);
2131    }
2132
2133    #[test]
2134    fn test_refresh_vector_index_after_dml() {
2135        // P52.38: the HNSW graph used to be populated only during CREATE VECTOR
2136        // INDEX; rows inserted later went un-indexed. refresh_vector_indexes_for_tables
2137        // must rebuild the graph from the live table so post-index DML is visible.
2138        let cat = TableCatalog::new();
2139        cat.create_node_table(
2140            "Item".into(),
2141            vec![
2142                ColumnDefinition {
2143                    compression: akar_common::enums::CompressionType::Uncompressed,
2144                    name: "id".into(),
2145                    logical_type: LogicalTypeID::Int64,
2146                    is_primary_key: true,
2147                },
2148                ColumnDefinition {
2149                    compression: akar_common::enums::CompressionType::Uncompressed,
2150                    name: "embedding".into(),
2151                    logical_type: LogicalTypeID::List,
2152                    is_primary_key: false,
2153                },
2154            ],
2155        );
2156
2157        let vec3 = |x: f64, y: f64, z: f64| Value::List(vec![Value::Double(x), Value::Double(y), Value::Double(z)]);
2158        {
2159            let mut t = cat.get_node_table_by_name_mut("Item").unwrap();
2160            t.insert_row(vec![Value::Int64(1), vec3(1.0, 0.0, 0.0)]).unwrap();
2161            t.insert_row(vec![Value::Int64(2), vec3(0.0, 1.0, 0.0)]).unwrap();
2162            t.insert_row(vec![Value::Int64(3), vec3(0.0, 0.0, 1.0)]).unwrap();
2163        }
2164
2165        cat.create_vector_index(
2166            "item_vec".into(),
2167            "Item".into(),
2168            "embedding".into(),
2169            DistanceMetric::Cosine,
2170            3,
2171        );
2172
2173        // Seed the index from the 3 pre-existing rows; the vector for row 2 must exist.
2174        cat.refresh_vector_indexes_for_tables(&[0]);
2175        {
2176            let vi = cat.get_vector_index(1).unwrap();
2177            assert_eq!(vi.hnsw().len(), 3);
2178            assert!(vi.hnsw().get_vector(2).is_some());
2179        }
2180
2181        // Rows inserted AFTER the index was created must become searchable.
2182        {
2183            let mut t = cat.get_node_table_by_name_mut("Item").unwrap();
2184            t.insert_row(vec![Value::Int64(4), vec3(1.0, 1.0, 0.0)]).unwrap();
2185            t.insert_row(vec![Value::Int64(5), vec3(0.0, 1.0, 1.0)]).unwrap();
2186        }
2187
2188        cat.refresh_vector_indexes_for_tables(&[0]);
2189        let vi = cat.get_vector_index(1).unwrap();
2190        assert_eq!(vi.hnsw().len(), 5);
2191        assert!(vi.hnsw().get_vector(4).is_some());
2192    }
2193}