use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use linkrs_core::error::storage::StorageErrorKind;
use linkrs_core::types::{LabelId, Timestamp};
use linkrs_core::{StorageError, StorageResult, Value};
use super::id_indexer::IdKey;
pub const MAX_WRITE_SCOPE_KEYS: usize = 4096;
type ScopeKey = (LabelId, IdKey);
type ScopeInsertRow = (u32, Vec<(std::sync::Arc<str>, Value)>);
type ScopeUpdateRow = Vec<(std::sync::Arc<str>, Value)>;
type ScopeIdKey = (LabelId, u32);
type ScopedInsert = (IdKey, u32, Vec<(std::sync::Arc<str>, Value)>);
#[derive(Debug, Default)]
pub struct WriteScope {
write_ts: Timestamp,
inserts: HashMap<ScopeKey, ScopeInsertRow>,
updates: HashMap<ScopeIdKey, ScopeUpdateRow>,
deletes: HashSet<ScopeIdKey>,
property_deletes: HashMap<ScopeIdKey, Vec<Arc<str>>>,
}
impl WriteScope {
pub fn new(write_ts: Timestamp) -> Self {
Self {
write_ts,
inserts: HashMap::new(),
updates: HashMap::new(),
deletes: HashSet::new(),
property_deletes: HashMap::new(),
}
}
pub fn write_ts(&self) -> Timestamp {
self.write_ts
}
pub fn ensure_same_write_ts(&self, ts: Timestamp) -> StorageResult<()> {
if self.write_ts != ts {
return Err(StorageError::invalid_operation(format!(
"write scope created for ts={} cannot serve ts={}: \
scopes are single-request staging areas, not shared buffers",
self.write_ts, ts
)));
}
Ok(())
}
pub(crate) fn from_staged_with_property_deletes(
write_ts: Timestamp,
inserts: HashMap<ScopeKey, ScopeInsertRow>,
updates: HashMap<ScopeIdKey, ScopeUpdateRow>,
deletes: HashSet<ScopeIdKey>,
property_deletes: HashMap<ScopeIdKey, Vec<Arc<str>>>,
) -> Self {
Self {
write_ts,
inserts,
updates,
deletes,
property_deletes,
}
}
pub fn len(&self) -> usize {
self.inserts.len() + self.updates.len() + self.deletes.len() + self.property_deletes.len()
}
pub fn is_empty(&self) -> bool {
self.inserts.is_empty()
&& self.updates.is_empty()
&& self.deletes.is_empty()
&& self.property_deletes.is_empty()
}
fn ensure_capacity(&self) -> StorageResult<()> {
if self.len() >= MAX_WRITE_SCOPE_KEYS {
return Err(StorageError::new(
StorageErrorKind::CapacityExceeded,
format!(
"write scope holds {} rows at the single-request limit {}: split the batch by label into smaller chunks instead of growing one request",
self.len(),
MAX_WRITE_SCOPE_KEYS,
),
));
}
Ok(())
}
pub fn stage_insert(
&mut self,
label: LabelId,
key: IdKey,
reserved_global_id: u32,
properties: Vec<(std::sync::Arc<str>, Value)>,
) -> StorageResult<()> {
if self.inserts.contains_key(&(label, key.clone())) {
return Err(StorageError::vertex_already_exists(format!(
"duplicate key in write scope: {:?}",
key
)));
}
self.ensure_capacity()?;
self.inserts
.insert((label, key), (reserved_global_id, properties));
Ok(())
}
pub fn stage_update(
&mut self,
label: LabelId,
global_id: u32,
properties: Vec<(Arc<str>, Value)>,
) -> StorageResult<()> {
for (name, _) in &properties {
if let Some(deleted) = self.property_deletes.get(&(label, global_id)) {
if deleted.iter().any(|d| d == name) {
return Err(StorageError::invalid_operation(format!(
"column '{}' staged for both update and delete: sets must stay disjoint",
name
)));
}
}
}
self.ensure_capacity_for_update(label, global_id)?;
self.updates
.entry((label, global_id))
.or_default()
.extend(properties);
Ok(())
}
fn ensure_capacity_for_update(&self, label: LabelId, global_id: u32) -> StorageResult<()> {
if self.updates.contains_key(&(label, global_id))
|| self.property_deletes.contains_key(&(label, global_id))
{
return Ok(());
}
self.ensure_capacity()
}
pub fn stage_property_deletes(
&mut self,
label: LabelId,
global_id: u32,
columns: Vec<Arc<str>>,
) -> StorageResult<()> {
if columns.is_empty() {
return Ok(());
}
if let Some(staged) = self.updates.get(&(label, global_id)) {
for name in &columns {
if staged.iter().any(|(existing, _)| existing == name) {
return Err(StorageError::invalid_operation(format!(
"column '{}' staged for both update and delete: sets must stay disjoint",
name
)));
}
}
}
if !self.property_deletes.contains_key(&(label, global_id)) {
self.ensure_capacity()?;
}
let entry = self.property_deletes.entry((label, global_id)).or_default();
for name in columns {
if !entry.contains(&name) {
entry.push(name);
}
}
Ok(())
}
pub fn stage_delete(&mut self, label: LabelId, global_id: u32) -> StorageResult<()> {
self.ensure_capacity()?;
self.deletes.insert((label, global_id));
Ok(())
}
pub fn take_inserts_for_label(&mut self, label: LabelId) -> Vec<ScopedInsert> {
let keys: Vec<(LabelId, IdKey)> = self
.inserts
.keys()
.filter(|(entry_label, _)| *entry_label == label)
.cloned()
.collect();
let mut out = Vec::with_capacity(keys.len());
for map_key in keys {
if let Some((reserved, props)) = self.inserts.remove(&map_key) {
out.push((map_key.1, reserved, props));
}
}
out
}
pub fn take_updates_for_label(
&mut self,
label: LabelId,
) -> Vec<(u32, Vec<(std::sync::Arc<str>, Value)>)> {
let keys: Vec<(LabelId, u32)> = self
.updates
.keys()
.filter(|(entry_label, _)| *entry_label == label)
.cloned()
.collect();
let mut out = Vec::with_capacity(keys.len());
for map_key in keys {
if let Some(props) = self.updates.remove(&map_key) {
out.push((map_key.1, props));
}
}
out
}
pub fn take_property_deletes_for_label(&mut self, label: LabelId) -> Vec<(u32, Vec<Arc<str>>)> {
let keys: Vec<(LabelId, u32)> = self
.property_deletes
.keys()
.filter(|(entry_label, _)| *entry_label == label)
.cloned()
.collect();
let mut out = Vec::with_capacity(keys.len());
for map_key in keys {
if let Some(cols) = self.property_deletes.remove(&map_key) {
out.push((map_key.1, cols));
}
}
out
}
pub fn take_deletes_for_label(&mut self, label: LabelId) -> Vec<u32> {
let keys: Vec<(LabelId, u32)> = self
.deletes
.iter()
.filter(|(entry_label, _)| *entry_label == label)
.cloned()
.collect();
let mut out = Vec::with_capacity(keys.len());
for map_key in &keys {
if self.deletes.remove(map_key) {
out.push(map_key.1);
}
}
out
}
pub fn staged_insert_ids_for_label(&self, label: LabelId) -> Vec<u32> {
self.inserts
.iter()
.filter(|((entry_label, _), _)| *entry_label == label)
.map(|(_, (reserved, _))| *reserved)
.collect()
}
pub fn labels(&self) -> Vec<LabelId> {
let mut labels: Vec<LabelId> = self
.inserts
.keys()
.map(|(label, _)| *label)
.chain(self.updates.keys().map(|(label, _)| *label))
.chain(self.deletes.iter().map(|(label, _)| *label))
.chain(self.property_deletes.keys().map(|(label, _)| *label))
.collect();
labels.sort_unstable();
labels.dedup();
labels
}
pub fn commit_label(&mut self, label: LabelId) {
self.inserts
.retain(|(entry_label, _), _| *entry_label != label);
self.updates
.retain(|(entry_label, _), _| *entry_label != label);
self.deletes
.retain(|(entry_label, _)| *entry_label != label);
self.property_deletes
.retain(|(entry_label, _), _| *entry_label != label);
}
pub fn rollback_label(&mut self, label: LabelId) -> Vec<u32> {
let dropped: Vec<u32> = self
.inserts
.iter()
.filter(|((entry_label, _), _)| *entry_label == label)
.map(|(_, (reserved, _))| *reserved)
.collect();
self.inserts
.retain(|(entry_label, _), _| *entry_label != label);
self.updates
.retain(|(entry_label, _), _| *entry_label != label);
self.deletes
.retain(|(entry_label, _)| *entry_label != label);
self.property_deletes
.retain(|(entry_label, _), _| *entry_label != label);
dropped
}
pub fn clear(&mut self) {
self.inserts.clear();
self.updates.clear();
self.deletes.clear();
self.property_deletes.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use linkrs_core::Value;
fn props(name: &str) -> Vec<(std::sync::Arc<str>, Value)> {
vec![("name".into(), Value::from(name))]
}
#[test]
fn scope_rejects_same_scope_duplicates() {
let mut scope = WriteScope::new(10);
scope
.stage_insert(1, IdKey::Text("a".to_string()), 100, props("a"))
.expect("first stage succeeds");
let duplicate = scope.stage_insert(1, IdKey::Text("a".to_string()), 101, props("a"));
assert!(duplicate.is_err());
assert_eq!(scope.len(), 1);
}
#[test]
fn scope_rejects_writes_past_capacity() {
let mut scope = WriteScope::new(10);
for index in 0..MAX_WRITE_SCOPE_KEYS {
scope
.stage_insert(1, IdKey::Int(index as i64), index as u32, Vec::new())
.expect("within capacity");
}
assert!(scope
.stage_insert(
1,
IdKey::Int(MAX_WRITE_SCOPE_KEYS as i64),
MAX_WRITE_SCOPE_KEYS as u32,
Vec::new()
)
.is_err());
}
#[test]
fn scope_capacity_error_guides_split_batching() {
use linkrs_core::error::storage::StorageErrorKind;
let mut scope = WriteScope::new(10);
for index in 0..MAX_WRITE_SCOPE_KEYS {
scope
.stage_insert(1, IdKey::Int(index as i64), index as u32, Vec::new())
.expect("within capacity");
}
let err = scope
.stage_insert(
1,
IdKey::Int(MAX_WRITE_SCOPE_KEYS as i64),
MAX_WRITE_SCOPE_KEYS as u32,
Vec::new(),
)
.unwrap_err();
assert_eq!(err.kind(), StorageErrorKind::CapacityExceeded);
let message = err.message().to_string();
assert!(message.contains(&MAX_WRITE_SCOPE_KEYS.to_string()));
assert!(message.contains("split"));
}
#[test]
fn scope_commit_and_rollback_drop_only_their_label() {
let mut scope = WriteScope::new(10);
scope
.stage_insert(1, IdKey::Int(1), 10, props("a"))
.unwrap();
scope
.stage_insert(2, IdKey::Int(1), 20, props("b"))
.unwrap();
scope.commit_label(1);
assert_eq!(scope.len(), 1);
let released = scope.rollback_label(2);
assert_eq!(released, vec![20]);
assert!(scope.is_empty());
}
#[test]
fn scope_rejects_cross_timestamp_reuse() {
let scope = WriteScope::new(10);
assert!(scope.ensure_same_write_ts(10).is_ok());
assert!(scope.ensure_same_write_ts(11).is_err());
}
#[test]
fn scope_stages_rows_and_hands_them_to_commit() {
let mut scope = WriteScope::new(10);
scope
.stage_insert(1, IdKey::Text("a".to_string()), 7, props("a"))
.unwrap();
scope
.stage_update(1, 7, vec![("age".into(), Value::from(3))])
.unwrap();
scope.stage_delete(1, 9).unwrap();
assert_eq!(scope.len(), 3);
let inserts = scope.take_inserts_for_label(1);
assert_eq!(inserts.len(), 1);
assert_eq!(inserts[0].1, 7);
assert_eq!(scope.take_updates_for_label(1).len(), 1);
assert_eq!(scope.take_deletes_for_label(1), vec![9]);
scope.commit_label(1);
assert!(scope.is_empty());
}
#[test]
fn scope_rollback_discards_every_kind() {
let mut scope = WriteScope::new(10);
scope.stage_insert(1, IdKey::Int(1), 5, props("a")).unwrap();
scope
.stage_update(1, 2, vec![("v".into(), Value::from(2))])
.unwrap();
scope.stage_delete(2, 3).unwrap();
let released = scope.rollback_label(1);
assert_eq!(released, vec![5]);
assert!(!scope.is_empty());
scope.rollback_label(2);
assert!(scope.is_empty());
}
}