use std::sync::Arc;
use parking_lot::RwLock;
use linkrs_core::types::{DataType, Timestamp};
use linkrs_core::StorageError;
pub mod column_batch;
pub mod predicates;
pub mod vec_cursors;
pub use column_batch::{ColumnValues, EdgeColumnBatch, PropertyColumn, VertexColumnBatch};
pub use predicates::{compare_stat_values, IndexPredicate, PredicateRange, ScanPredicate};
pub use vec_cursors::{VecEdgeCursor, VecVertexCursor};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScanTarget {
Vertex,
Edge { edge_type: Option<String> },
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RequiredProperty {
pub name: Arc<str>,
pub column_id: Option<i32>,
pub data_type: Option<DataType>,
pub schema_version: u64,
}
impl RequiredProperty {
pub fn new(name: impl Into<Arc<str>>) -> Self {
let name = name.into();
Self {
name,
column_id: None,
data_type: None,
schema_version: 0,
}
}
pub fn with_metadata(
name: impl Into<Arc<str>>,
column_id: Option<i32>,
data_type: Option<DataType>,
schema_version: u64,
) -> Self {
Self {
name: name.into(),
column_id,
data_type,
schema_version,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn column_id(&self) -> Option<i32> {
self.column_id
}
pub fn data_type(&self) -> Option<&DataType> {
self.data_type.as_ref()
}
pub fn schema_version(&self) -> u64 {
self.schema_version
}
}
#[derive(Debug, Clone, Default)]
pub struct ScanOptions {
pub limit: Option<usize>,
pub offset: usize,
pub batch_size: usize,
pub vertex_id_range: Option<std::ops::Range<i64>>,
pub edge_src_id_range: Option<std::ops::Range<i64>>,
pub edge_type: Option<String>,
pub tag: Option<String>,
pub projection: Option<Vec<RequiredProperty>>,
pub topology_only: bool,
pub read_timestamp: Option<Timestamp>,
pub predicate: Option<Vec<ScanPredicate>>,
pub column_block_mode: bool,
pub internal_id_allowlist: Option<Vec<u32>>,
pub edge_table_allowlist: Option<Vec<crate::engine::data_store::EdgeTableKey>>,
pub shard_slice: Option<ShardSlice>,
pub edge_src_slice: Option<ShardSlice>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShardSlice {
pub partition_id: usize,
pub partition_count: usize,
}
impl ShardSlice {
pub fn new(partition_id: usize, partition_count: usize) -> Self {
Self {
partition_id,
partition_count,
}
}
}
impl ScanOptions {
pub const DEFAULT_BATCH_SIZE: usize = 1024;
pub fn new() -> Self {
Self::default()
}
pub fn with_edge_type(mut self, edge_type: String) -> Self {
self.edge_type = Some(edge_type);
self
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
pub fn with_projection_named(mut self, projection: Vec<String>) -> Self {
self.projection = Some(projection.into_iter().map(RequiredProperty::new).collect());
self
}
pub fn with_projection(mut self, projection: Vec<RequiredProperty>) -> Self {
self.projection = Some(projection);
self
}
pub fn with_tag(mut self, tag: String) -> Self {
self.tag = Some(tag);
self
}
pub fn with_internal_id_allowlist(mut self, ids: Vec<u32>) -> Self {
self.internal_id_allowlist = Some(ids);
self
}
pub fn with_shard_slice(mut self, slice: ShardSlice) -> Self {
self.shard_slice = Some(slice);
self
}
pub fn with_edge_src_slice(mut self, slice: ShardSlice) -> Self {
self.edge_src_slice = Some(slice);
self
}
pub fn batch_size(&self) -> usize {
if self.batch_size == 0 {
Self::DEFAULT_BATCH_SIZE
} else {
self.batch_size
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum PartitionSelector {
#[default]
All,
Shards(Vec<u32>),
KeyRange {
lower: Option<Vec<u8>>,
upper: Option<Vec<u8>>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum IndexRow {
RowId(linkrs_core::wal::EntityRef),
Covering {
entity_ref: linkrs_core::wal::EntityRef,
columns: Vec<(std::sync::Arc<str>, linkrs_core::Value)>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct IndexScanPlan {
pub space: String,
pub index_id: u64,
pub predicate: IndexPredicate,
pub partition: PartitionSelector,
pub partition_id_range: Option<std::ops::Range<i64>>,
pub projection: Option<Vec<Arc<str>>>,
pub limit: Option<usize>,
pub offset: usize,
pub read_timestamp: Timestamp,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FlatVertexRecord {
pub vid: linkrs_core::types::VertexId,
pub internal_id: i64,
pub tag_name: String,
pub props: Vec<(std::sync::Arc<str>, linkrs_core::Value)>,
}
pub(crate) fn column_data_type(values: &[Option<linkrs_core::Value>]) -> DataType {
for v in values.iter().flatten() {
let ty = v.get_type();
if !matches!(ty, DataType::Empty | DataType::Null) {
return ty;
}
}
DataType::Empty
}
pub trait VertexCursor: Send + std::fmt::Debug {
fn next_batch(&mut self, batch_size: usize) -> Result<Vec<linkrs_core::Vertex>, StorageError>;
fn next_flat_batch(
&mut self,
batch_size: usize,
) -> Result<Vec<FlatVertexRecord>, StorageError> {
Ok(self
.next_batch(batch_size)?
.into_iter()
.map(|v| FlatVertexRecord {
vid: v.vid,
internal_id: v.vid.as_internal_u32().map(|id| id as i64).unwrap_or(-1),
tag_name: v.tag.name.clone(),
props: v.tag.properties.clone().into_iter().collect(),
})
.collect())
}
fn next_column_batch(
&mut self,
prop_names: &[std::sync::Arc<str>],
batch_size: usize,
) -> Result<VertexColumnBatch, StorageError> {
let records = self.next_flat_batch(batch_size)?;
let row_count = records.len();
let mut batch = VertexColumnBatch {
vids: Vec::with_capacity(row_count),
internal_ids: Vec::with_capacity(row_count),
tag_names: Vec::with_capacity(row_count),
columns: Vec::with_capacity(prop_names.len()),
};
let mut per_name: Vec<Vec<Option<linkrs_core::Value>>> = prop_names
.iter()
.map(|_| Vec::with_capacity(row_count))
.collect();
for record in records {
batch.tag_names.push(record.tag_name.clone());
batch.vids.push(record.vid);
batch.internal_ids.push(record.internal_id);
for (i, name) in prop_names.iter().enumerate() {
let value = record
.props
.iter()
.find(|(n, _)| n.as_ref() == name.as_ref())
.map(|(_, v)| v.clone());
per_name[i].push(value);
}
}
for (i, name) in prop_names.iter().enumerate() {
batch.columns.push(PropertyColumn {
name: name.clone(),
data_type: column_data_type(&per_name[i]),
values: ColumnValues::General(std::mem::take(&mut per_name[i])),
});
}
Ok(batch)
}
}
pub trait EdgeCursor: Send + std::fmt::Debug {
fn next_batch(&mut self, batch_size: usize) -> Result<Vec<linkrs_core::Edge>, StorageError>;
fn next_column_batch(
&mut self,
prop_names: &[std::sync::Arc<str>],
batch_size: usize,
) -> Result<EdgeColumnBatch, StorageError> {
let edges = self.next_batch(batch_size)?;
let row_count = edges.len();
let mut batch = EdgeColumnBatch {
srcs: Vec::with_capacity(row_count),
dsts: Vec::with_capacity(row_count),
edge_types: Vec::with_capacity(row_count),
rankings: Vec::with_capacity(row_count),
columns: Vec::with_capacity(prop_names.len()),
};
let mut per_name: Vec<Vec<Option<linkrs_core::Value>>> = prop_names
.iter()
.map(|_| Vec::with_capacity(row_count))
.collect();
for edge in edges {
batch.srcs.push(edge.src);
batch.dsts.push(edge.dst);
batch.edge_types.push(edge.edge_type);
batch.rankings.push(edge.ranking);
for (i, name) in prop_names.iter().enumerate() {
per_name[i].push(edge.props.get(name.as_ref()).cloned());
}
}
for (i, name) in prop_names.iter().enumerate() {
batch.columns.push(PropertyColumn {
name: name.clone(),
data_type: column_data_type(&per_name[i]),
values: ColumnValues::General(std::mem::take(&mut per_name[i])),
});
}
Ok(batch)
}
fn malformed_skipped(&self) -> u64 {
0
}
}
pub trait IndexCursor: Send + std::fmt::Debug {
type Row: Send;
fn next_batch(&mut self, batch_size: usize) -> Result<Vec<Self::Row>, StorageError>;
fn stale_skipped(&self) -> u64 {
0
}
fn invisible_skipped(&self) -> u64 {
0
}
fn malformed_skipped(&self) -> u64 {
0
}
fn is_exhausted(&self) -> bool {
true
}
}
pub fn open_vertex_scan<S: crate::StorageReader + ?Sized>(
storage: &Arc<RwLock<S>>,
space: &str,
options: &ScanOptions,
) -> Result<Box<dyn VertexCursor>, StorageError> {
let reader = storage.read();
reader.create_vertex_cursor(space, options)
}
pub fn open_edge_scan<S: crate::StorageReader + ?Sized>(
storage: &Arc<RwLock<S>>,
space: &str,
options: &ScanOptions,
) -> Result<Box<dyn EdgeCursor>, StorageError> {
let reader = storage.read();
reader.create_edge_cursor(space, options)
}
pub fn open_index_cursor<S: crate::StorageReader + ?Sized>(
storage: &Arc<RwLock<S>>,
plan: &IndexScanPlan,
) -> Result<Box<dyn IndexCursor<Row = IndexRow>>, StorageError> {
let reader = storage.read();
reader.create_index_cursor(plan)
}
#[cfg(test)]
mod tests {
use super::*;
use linkrs_core::types::storage_ids::VertexId;
use linkrs_core::{Tag, Value, Vertex};
use std::collections::HashMap;
#[test]
fn next_flat_batch_default_converts_vertices() {
let mut props = HashMap::new();
props.insert("age".into(), Value::BigInt(30));
let vertex = Vertex::new(
VertexId::try_from_int64(1).expect("test vertex id"),
Tag::new("person".into(), props),
);
let mut cursor = VecVertexCursor::new(vec![vertex]);
let batch = cursor
.next_flat_batch(10)
.expect("flat batch should succeed");
assert_eq!(batch.len(), 1);
let rec = &batch[0];
assert_eq!(
rec.vid,
VertexId::try_from_int64(1).expect("test vertex id")
);
assert_eq!(rec.internal_id, 1);
assert_eq!(rec.tag_name, "person");
assert_eq!(rec.props, vec![("age".into(), Value::BigInt(30))]);
}
#[test]
fn next_flat_batch_empty_when_exhausted() {
let mut cursor = VecVertexCursor::new(Vec::new());
let batch = cursor
.next_flat_batch(10)
.expect("flat batch should succeed");
assert!(batch.is_empty());
}
}