Skip to main content

uni_store/storage/
adjacency.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4use crate::backend::StorageBackend;
5use crate::backend::table_names;
6use crate::backend::types::{FilterExpr, Scalar, ScanRequest, WriteMode};
7use anyhow::{Result, anyhow};
8use arrow_array::{ListArray, RecordBatch, UInt64Array};
9use arrow_schema::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
10use std::collections::HashMap;
11use std::sync::Arc;
12use uni_common::core::id::{Eid, Vid};
13
14/// Type alias for adjacency list data (neighbors, edge_ids).
15type AdjacencyLists = (Vec<Vid>, Vec<Eid>);
16
17/// Type alias for grouped adjacency data by source vertex.
18type GroupedAdjacencyLists = HashMap<Vid, (Vec<Vid>, Vec<Eid>)>;
19
20/// Downcast the neighbors and edge_ids list columns from a RecordBatch.
21fn downcast_adjacency_lists(batch: &RecordBatch) -> Result<(&ListArray, &ListArray)> {
22    let neighbors_list = batch
23        .column_by_name("neighbors")
24        .ok_or(anyhow!("Missing neighbors"))?
25        .as_any()
26        .downcast_ref::<ListArray>()
27        .ok_or(anyhow!("Invalid neighbors type"))?;
28
29    let edge_ids_list = batch
30        .column_by_name("edge_ids")
31        .ok_or(anyhow!("Missing edge_ids"))?
32        .as_any()
33        .downcast_ref::<ListArray>()
34        .ok_or(anyhow!("Invalid edge_ids type"))?;
35
36    Ok((neighbors_list, edge_ids_list))
37}
38
39/// Extract (neighbors, edge_ids) from a single row of the adjacency list columns.
40fn extract_row_adjacency(
41    neighbors_list: &ListArray,
42    edge_ids_list: &ListArray,
43    row_idx: usize,
44) -> Result<(Vec<Vid>, Vec<Eid>)> {
45    let neighbors_array = neighbors_list.value(row_idx);
46    let neighbors_uint64 = neighbors_array
47        .as_any()
48        .downcast_ref::<UInt64Array>()
49        .ok_or(anyhow!("Invalid neighbors inner type"))?;
50
51    let edge_ids_array = edge_ids_list.value(row_idx);
52    let edge_ids_uint64 = edge_ids_array
53        .as_any()
54        .downcast_ref::<UInt64Array>()
55        .ok_or(anyhow!("Invalid edge_ids inner type"))?;
56
57    let neighbors = (0..neighbors_uint64.len())
58        .map(|i| Vid::from(neighbors_uint64.value(i)))
59        .collect();
60    let eids = (0..edge_ids_uint64.len())
61        .map(|i| Eid::from(edge_ids_uint64.value(i)))
62        .collect();
63
64    Ok((neighbors, eids))
65}
66
67/// Extract adjacency data (neighbors, edge IDs) from a single row of a RecordBatch.
68///
69/// Returns `None` if the batch is empty or columns are missing.
70fn extract_adjacency_from_batch(batch: &RecordBatch) -> Result<Option<AdjacencyLists>> {
71    if batch.num_rows() == 0 {
72        return Ok(None);
73    }
74
75    let (neighbors_list, edge_ids_list) = downcast_adjacency_lists(batch)?;
76
77    let mut all_neighbors = Vec::new();
78    let mut all_eids = Vec::new();
79
80    for row_idx in 0..batch.num_rows() {
81        let (neighbors, eids) = extract_row_adjacency(neighbors_list, edge_ids_list, row_idx)?;
82        all_neighbors.extend(neighbors);
83        all_eids.extend(eids);
84    }
85
86    Ok(Some((all_neighbors, all_eids)))
87}
88
89/// Extract adjacency data from a batch, grouped by src_vid.
90///
91/// Returns a HashMap mapping each src_vid to its (neighbors, edge_ids).
92fn extract_adjacency_from_batch_grouped(batch: &RecordBatch) -> Result<GroupedAdjacencyLists> {
93    if batch.num_rows() == 0 {
94        return Ok(HashMap::new());
95    }
96
97    let src_vid_col = batch
98        .column_by_name("src_vid")
99        .ok_or(anyhow!("Missing src_vid"))?
100        .as_any()
101        .downcast_ref::<UInt64Array>()
102        .ok_or(anyhow!("Invalid src_vid type"))?;
103
104    let (neighbors_list, edge_ids_list) = downcast_adjacency_lists(batch)?;
105
106    let mut result: HashMap<Vid, (Vec<Vid>, Vec<Eid>)> = HashMap::new();
107
108    for row_idx in 0..batch.num_rows() {
109        let src_vid = Vid::from(src_vid_col.value(row_idx));
110        let (neighbors, eids) = extract_row_adjacency(neighbors_list, edge_ids_list, row_idx)?;
111        result.insert(src_vid, (neighbors, eids));
112    }
113
114    Ok(result)
115}
116
117pub struct AdjacencyDataset {
118    edge_type: String,
119    direction: String,
120    /// Lance branch for branched reads. `None` = primary.
121    #[cfg_attr(not(feature = "lance-backend"), allow(dead_code))]
122    branch: Option<String>,
123}
124
125impl AdjacencyDataset {
126    /// Construct a primary (unbranched) adjacency dataset handle.
127    ///
128    /// The on-disk dataset is per-`(edge_type, direction)` — the `label`
129    /// argument is vestigial (adjacency is not per-label) and kept only for
130    pub fn new(_base_uri: &str, edge_type: &str, _label: &str, direction: &str) -> Self {
131        Self {
132            edge_type: edge_type.to_string(),
133            direction: direction.to_string(),
134            branch: None,
135        }
136    }
137
138    /// Construct an adjacency dataset that reads from a Lance branch.
139    pub fn new_branched(
140        base_uri: &str,
141        edge_type: &str,
142        label: &str,
143        direction: &str,
144        branch: impl Into<String>,
145    ) -> Self {
146        let mut ds = Self::new(base_uri, edge_type, label, direction);
147        ds.branch = Some(branch.into());
148        ds
149    }
150
151    pub fn get_arrow_schema(&self) -> Arc<ArrowSchema> {
152        let fields = vec![
153            Field::new("src_vid", ArrowDataType::UInt64, false),
154            // neighbors: list<uint64>
155            Field::new(
156                "neighbors",
157                ArrowDataType::List(Arc::new(Field::new("item", ArrowDataType::UInt64, true))),
158                false,
159            ),
160            // edge_ids: list<uint64>
161            Field::new(
162                "edge_ids",
163                ArrowDataType::List(Arc::new(Field::new("item", ArrowDataType::UInt64, true))),
164                false,
165            ),
166        ];
167
168        Arc::new(ArrowSchema::new(fields))
169    }
170
171    // ========================================================================
172    // Backend-agnostic Methods
173    // ========================================================================
174
175    /// Read adjacency data for a vertex from the storage backend.
176    ///
177    /// Returns `None` if the table doesn't exist or no data for the vertex.
178    pub async fn read_adjacency_backend(
179        &self,
180        backend: &dyn StorageBackend,
181        vid: Vid,
182    ) -> Result<Option<(Vec<Vid>, Vec<Eid>)>> {
183        let table_name = table_names::adjacency_table_name(&self.edge_type, &self.direction);
184
185        if !backend.table_exists(&table_name).await? {
186            return Ok(None);
187        }
188
189        let filter = FilterExpr::equals("src_vid", Scalar::UInt(vid.as_u64()));
190        let batches = backend
191            .scan(ScanRequest::all(&table_name).with_filter(filter))
192            .await?;
193
194        for batch in batches {
195            if let Some(result) = extract_adjacency_from_batch(&batch)? {
196                return Ok(Some(result));
197            }
198        }
199
200        Ok(None)
201    }
202
203    /// Read adjacency data for multiple vertices in a single batch query.
204    ///
205    /// Returns a HashMap mapping each vid to its (neighbors, edge_ids).
206    /// VIDs with no adjacency data will not be in the map.
207    pub async fn read_adjacency_backend_batch(
208        &self,
209        backend: &dyn StorageBackend,
210        vids: &[Vid],
211    ) -> Result<HashMap<Vid, (Vec<Vid>, Vec<Eid>)>> {
212        if vids.is_empty() {
213            return Ok(HashMap::new());
214        }
215
216        let table_name = table_names::adjacency_table_name(&self.edge_type, &self.direction);
217
218        if !backend.table_exists(&table_name).await? {
219            return Ok(HashMap::new());
220        }
221
222        let filter = FilterExpr::one_of("src_vid", vids.iter().map(|v| Scalar::UInt(v.as_u64())));
223        let batches = backend
224            .scan(ScanRequest::all(&table_name).with_filter(filter))
225            .await?;
226
227        let mut result = HashMap::new();
228        for batch in batches {
229            let batch_result = extract_adjacency_from_batch_grouped(&batch)?;
230            result.extend(batch_result);
231        }
232
233        Ok(result)
234    }
235
236    /// Open or create an adjacency table via the storage backend.
237    pub async fn open_or_create(&self, backend: &dyn StorageBackend) -> Result<()> {
238        let table_name = table_names::adjacency_table_name(&self.edge_type, &self.direction);
239        let arrow_schema = self.get_arrow_schema();
240        backend
241            .open_or_create_table(&table_name, arrow_schema)
242            .await
243    }
244
245    /// Write a chunk to an adjacency table.
246    ///
247    /// Creates the table if it doesn't exist, otherwise appends to it.
248    pub async fn write_chunk(
249        &self,
250        backend: &dyn StorageBackend,
251        batch: RecordBatch,
252    ) -> Result<()> {
253        let table_name = table_names::adjacency_table_name(&self.edge_type, &self.direction);
254        if backend.table_exists(&table_name).await? {
255            backend
256                .write(&table_name, vec![batch], WriteMode::Append)
257                .await
258        } else {
259            backend.create_table(&table_name, vec![batch]).await
260        }
261    }
262
263    /// Get the table name for this adjacency dataset.
264    pub fn table_name(&self) -> String {
265        table_names::adjacency_table_name(&self.edge_type, &self.direction)
266    }
267
268    /// Replace an adjacency table's contents atomically.
269    ///
270    /// Used by compaction to rewrite the table with merged data.
271    pub async fn replace(&self, backend: &dyn StorageBackend, batch: RecordBatch) -> Result<()> {
272        let table_name = self.table_name();
273        let arrow_schema = self.get_arrow_schema();
274        backend
275            .replace_table_atomic(&table_name, vec![batch], arrow_schema)
276            .await
277    }
278}