use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::{mpsc, oneshot};
use crate::error::{classify, DbError, Result, WriteOp};
use crate::graph::edge::EdgeAssertion;
use crate::integrity::{rebuild_current, RebuildReport};
use crate::schema::migrations;
use crate::temporal::archive::{archive, ArchiveReport};
use crate::temporal::interval::Interval;
use crate::temporal::snapshot::{self, SnapshotCadence};
use crate::util::clock::{Clock, SystemClock};
use crate::util::timestamp;
use crate::vector::ModelName;
pub mod chunk_rows {
pub const EDGES: usize = 90;
pub const CONCEPTS: usize = 70;
pub const ANNOTATIONS: usize = 600;
pub const EMBEDDINGS: usize = 30;
}
pub const CHUNK_BUDGET: std::time::Duration = std::time::Duration::from_millis(3);
pub const BULK_ATOMIC_WARN_HOLD: std::time::Duration = std::time::Duration::from_millis(250);
pub fn estimated_bulk_hold(edges: &[EdgeAssertion]) -> std::time::Duration {
let rows = edges.len() as u64;
let all_pairs = rows.saturating_mul(rows.saturating_sub(1)) / 2;
let mut groups: std::collections::HashMap<(&str, &str, &str), u64> =
std::collections::HashMap::new();
for e in edges {
*groups
.entry((&e.source, &e.target, &e.edge_type))
.or_insert(0) += 1;
}
let matching: u64 = groups.values().map(|&g| g * (g - 1) / 2).sum();
let mismatched = all_pairs - matching;
std::time::Duration::from_nanos(
(73_000u64.saturating_mul(rows))
.saturating_add(mismatched.saturating_mul(11) / 2)
.saturating_add(matching.saturating_mul(86)),
)
}
pub const MAX_ARCHIVE_SESSIONS: usize = 4_096;
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptUpsert {
pub id: String,
pub title: String,
pub content: String,
pub embedding_model: Option<String>,
pub valid_from: String,
pub valid_to: String,
pub retired: bool,
}
impl ConceptUpsert {
pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
Self {
id: id.into(),
title: title.into(),
content: String::new(),
embedding_model: None,
valid_from: String::new(),
valid_to: timestamp::OPEN_SENTINEL.to_string(),
retired: false,
}
}
pub fn content(mut self, content: impl Into<String>) -> Self {
self.content = content.into();
self
}
pub fn embedding_model(mut self, model: impl Into<String>) -> Self {
self.embedding_model = Some(model.into());
self
}
pub fn valid_from(mut self, ts: impl Into<String>) -> Self {
self.valid_from = ts.into();
self
}
pub fn valid_to(mut self, ts: impl Into<String>) -> Self {
self.valid_to = ts.into();
self
}
pub fn retired(mut self, retired: bool) -> Self {
self.retired = retired;
self
}
pub fn normalized(mut self) -> Result<Self> {
crate::util::ids::validate_id(&self.id)?;
self.valid_from = timestamp::normalize(&self.valid_from)?;
self.valid_to = timestamp::normalize(&self.valid_to)?;
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Annotation {
pub concept_id: String,
pub label: String,
pub value: String,
}
impl Annotation {
pub fn new(
concept_id: impl Into<String>,
label: impl Into<String>,
value: impl Into<String>,
) -> Self {
Self {
concept_id: concept_id.into(),
label: label.into(),
value: value.into(),
}
}
}
pub enum HighPriCommand {
AssertEdge {
edge: EdgeAssertion,
responder: oneshot::Sender<Result<()>>,
},
RetireEdge {
source: String,
target: String,
edge_type: String,
valid_from: String,
valid_to: String,
responder: oneshot::Sender<Result<()>>,
},
UpsertConcept {
concept: ConceptUpsert,
responder: oneshot::Sender<Result<()>>,
},
WriteBulkAtomic {
edges: Vec<EdgeAssertion>,
responder: oneshot::Sender<Result<usize>>,
},
RebuildCurrent {
responder: oneshot::Sender<Result<RebuildReport>>,
},
RegisterModel {
model: ModelName,
dim: usize,
responder: oneshot::Sender<Result<()>>,
},
Shutdown {
responder: oneshot::Sender<Result<()>>,
},
}
pub enum LowPriCommand {
WriteConceptsChunk {
chunk: Vec<ConceptUpsert>,
responder: oneshot::Sender<Result<usize>>,
},
WriteAnalyticsChunk {
chunk: Vec<Annotation>,
responder: oneshot::Sender<Result<usize>>,
},
UpsertEmbeddingChunk {
model: ModelName,
chunk: Vec<(String, Vec<f32>)>,
responder: oneshot::Sender<Result<usize>>,
},
BulkImportChunk {
chunk: Vec<EdgeAssertion>,
responder: oneshot::Sender<Result<usize>>,
},
Archive {
cutoff: String,
archive_path: PathBuf,
responder: oneshot::Sender<Result<ArchiveReport>>,
},
RebuildFts {
responder: oneshot::Sender<Result<()>>,
},
ShadowRebuild {
step: crate::integrity::ShadowStep,
responder: oneshot::Sender<Result<crate::integrity::ShadowOutcome>>,
},
}
enum LoopCtl {
Continue,
Break,
}
pub struct Database {
db: libsql::Database,
path: PathBuf,
read_conn: libsql::Connection,
highpri_tx: mpsc::Sender<HighPriCommand>,
lowpri_tx: mpsc::Sender<LowPriCommand>,
clock: Arc<dyn Clock>,
archive_path: PathBuf,
snapshots_dir: PathBuf,
schema_version: u32,
writer: Option<tokio::task::JoinHandle<Result<()>>>,
cadence_stop: Option<tokio::sync::watch::Sender<bool>>,
cadence: Option<tokio::task::JoinHandle<()>>,
closed: bool,
#[cfg_attr(not(feature = "metrics"), allow(dead_code))]
shared: Arc<ActorShared>,
}
impl Database {
pub async fn open(path: impl AsRef<Path>) -> Result<Self> {
Self::open_with_cadence(path, Some(SnapshotCadence::default())).await
}
pub async fn open_with_cadence(
path: impl AsRef<Path>,
cadence: Option<SnapshotCadence>,
) -> Result<Self> {
Self::open_inner(path.as_ref(), cadence, None).await
}
pub async fn open_with_clock(
path: impl AsRef<Path>,
cadence: Option<SnapshotCadence>,
clock: Arc<dyn Clock>,
) -> Result<Self> {
Self::open_inner(path.as_ref(), cadence, Some(clock)).await
}
async fn open_inner(
path: &Path,
cadence: Option<SnapshotCadence>,
injected: Option<Arc<dyn Clock>>,
) -> Result<Self> {
let db = libsql::Builder::new_local(path).build().await?;
let write_conn = configure(db.connect()?).await?;
let read_conn = configure(db.connect()?).await?;
read_conn.execute("PRAGMA query_only = ON", ()).await?;
let migration = migrations::run(&write_conn).await?;
let (highpri_tx, highpri_rx) = mpsc::channel(256);
let (lowpri_tx, lowpri_rx) = mpsc::channel(64);
let clock: Arc<dyn Clock> = match injected {
Some(clock) => {
if let Some(floor) = crate::util::clock::recorded_at_floor(&read_conn).await? {
clock.raise_floor(floor);
}
clock
}
None => Arc::new(SystemClock::new(&read_conn).await?),
};
let shared = Arc::new(ActorShared::default());
let writer = tokio::spawn(run_writer_actor(
write_conn,
Arc::clone(&clock),
highpri_rx,
lowpri_rx,
Arc::clone(&shared),
));
let archive_path = derive_archive_path(path);
let snapshots_dir = derive_snapshots_dir(path);
let (cadence_stop, cadence) = match cadence {
Some(cadence) => {
let cadence_conn = configure(db.connect()?).await?;
cadence_conn.execute("PRAGMA query_only = ON", ()).await?;
let (tx, rx) = tokio::sync::watch::channel(false);
let handle = tokio::spawn(snapshot::run_cadence(
cadence_conn,
snapshots_dir.clone(),
archive_path.clone(),
cadence,
rx,
));
(Some(tx), Some(handle))
}
None => (None, None),
};
let handle = Self {
db,
path: path.to_path_buf(),
read_conn,
highpri_tx,
lowpri_tx,
clock,
archive_path,
snapshots_dir,
schema_version: migrations::current_version(),
writer: Some(writer),
cadence_stop,
cadence,
closed: false,
shared,
};
if migration.upgraded() && handle.cadence.is_some() {
let ts = handle.clock.now();
let archive = handle
.archive_path
.exists()
.then_some(handle.archive_path.as_path());
match snapshot::write_final(&handle.read_conn, &handle.snapshots_dir, &ts, archive)
.await
{
Ok(path) => tracing::info!(
"schema moved v{} -> v{}; re-anchored snapshots at {:?}",
migration.from,
migration.to,
path
),
Err(e) => tracing::warn!(
"schema moved v{} -> v{} but the re-anchor failed: {e}. \
Reconstruction stays correct and folds from genesis until the \
cadence writes one.",
migration.from,
migration.to
),
}
}
Ok(handle)
}
pub fn read_conn(&self) -> &libsql::Connection {
&self.read_conn
}
pub fn path(&self) -> &Path {
&self.path
}
pub async fn diagnostic_conn(&self) -> Result<libsql::Connection> {
let fail = |reason: String| DbError::DiagnosticConn {
path: self.path.display().to_string(),
reason,
};
if !self.path.exists() {
return Err(fail(
"the file does not exist, and a read-only open cannot create it".to_string(),
));
}
let db = libsql::Builder::new_local(&self.path)
.flags(libsql::OpenFlags::SQLITE_OPEN_READ_ONLY)
.build()
.await
.map_err(|e| fail(e.to_string()))?;
db.connect().map_err(|e| fail(e.to_string()))
}
pub async fn verify_snapshot_chain(&self, ts: &str) -> Result<crate::temporal::ChainCheck> {
let archive = self
.archive_path
.exists()
.then_some(self.archive_path.as_path());
crate::temporal::verify_snapshot_chain(&self.read_conn, ts, archive, &self.snapshots_dir)
.await
}
pub fn clock(&self) -> &Arc<dyn Clock> {
&self.clock
}
pub fn schema_version(&self) -> u32 {
self.schema_version
}
pub fn archive_path(&self) -> &Path {
&self.archive_path
}
pub fn snapshots_dir(&self) -> &Path {
&self.snapshots_dir
}
#[cfg(feature = "metrics")]
pub fn metrics(&self) -> crate::metrics::MetricsSnapshot {
self.shared.metrics.snapshot()
}
#[doc(hidden)]
pub fn raw(&self) -> &libsql::Database {
&self.db
}
pub async fn assert_edge(&self, edge: EdgeAssertion) -> Result<()> {
let edge = edge.normalized()?;
self.high(|responder| HighPriCommand::AssertEdge { edge, responder })
.await
}
pub async fn retire_edge(
&self,
source: impl Into<String>,
target: impl Into<String>,
edge_type: impl Into<String>,
valid_from: &str,
valid_to: &str,
) -> Result<()> {
let edge_type = edge_type.into();
crate::graph::edge::validate_edge_type(&edge_type)?;
let valid_from = timestamp::normalize(valid_from)?;
let valid_to = timestamp::normalize(valid_to)?;
let (source, target) = (source.into(), target.into());
self.high(|responder| HighPriCommand::RetireEdge {
source,
target,
edge_type,
valid_from,
valid_to,
responder,
})
.await
}
pub async fn upsert_concept(&self, concept: ConceptUpsert) -> Result<()> {
let concept = concept.normalized()?;
self.high(|responder| HighPriCommand::UpsertConcept { concept, responder })
.await
}
pub async fn write_bulk_atomic(&self, edges: Vec<EdgeAssertion>) -> Result<usize> {
let estimate = estimated_bulk_hold(&edges);
if estimate > BULK_ATOMIC_WARN_HOLD {
tracing::warn!(
rows = edges.len(),
estimated_hold_ms = estimate.as_millis() as u64,
"write_bulk_atomic will hold the write actor for roughly \
{estimate:?} — it is atomic by contract (D-014) and cannot be \
chunked. Every other writer waits that long. Use bulk_import \
if the batch does not need to be all-or-nothing."
);
}
let edges = normalize_all(edges)?;
self.high(|responder| HighPriCommand::WriteBulkAtomic { edges, responder })
.await
}
pub async fn rebuild_current(&self) -> Result<RebuildReport> {
self.high(|responder| HighPriCommand::RebuildCurrent { responder })
.await
}
pub async fn rebuild_current_chunked(&self) -> Result<RebuildReport> {
use crate::integrity::{ShadowOutcome, ShadowStep};
let ShadowOutcome::Started { build_start, epoch } =
self.shadow_step(ShadowStep::Begin).await?
else {
return Err(DbError::WriterDroppedResponder);
};
let mut after: Option<String> = None;
loop {
let ShadowOutcome::Filled { last } = self
.shadow_step(ShadowStep::Fill {
after: after.take(),
})
.await?
else {
return Err(DbError::WriterDroppedResponder);
};
match last {
Some(last) => after = Some(last),
None => break,
}
}
let ShadowOutcome::Swapped { rows } = self
.shadow_step(ShadowStep::Swap { build_start, epoch })
.await?
else {
return Err(DbError::WriterDroppedResponder);
};
Ok(RebuildReport {
rows_rebuilt: rows,
drift_after: 0,
})
}
pub async fn shadow_step(
&self,
step: crate::integrity::ShadowStep,
) -> Result<crate::integrity::ShadowOutcome> {
self.low(|responder| LowPriCommand::ShadowRebuild { step, responder })
.await
}
pub async fn bulk_import(&self, edges: Vec<EdgeAssertion>) -> Result<usize> {
let edges = normalize_all(edges)?;
let chunks: Vec<_> = edges.chunks(chunk_rows::EDGES).map(<[_]>::to_vec).collect();
self.low_chunked(chunks, |chunk, responder| LowPriCommand::BulkImportChunk {
chunk,
responder,
})
.await
}
pub async fn write_concepts(&self, concepts: Vec<ConceptUpsert>) -> Result<usize> {
let concepts: Vec<ConceptUpsert> = concepts
.into_iter()
.map(ConceptUpsert::normalized)
.collect::<Result<_>>()?;
let chunks: Vec<_> = concepts
.chunks(chunk_rows::CONCEPTS)
.map(<[_]>::to_vec)
.collect();
self.low_chunked(chunks, |chunk, responder| {
LowPriCommand::WriteConceptsChunk { chunk, responder }
})
.await
}
pub async fn reconstruct(&self, ts: &str) -> Result<crate::temporal::MaterializedState> {
let ts = timestamp::normalize(ts)?;
crate::temporal::reconstruct(
&self.read_conn,
&ts,
Some(&self.archive_path),
Some(&self.snapshots_dir),
)
.await
}
pub async fn register_model(&self, model: &ModelName, dim: usize) -> Result<()> {
let model = model.clone();
self.high(|responder| HighPriCommand::RegisterModel {
model,
dim,
responder,
})
.await
}
pub async fn upsert_embeddings(
&self,
model: &ModelName,
rows: Vec<(String, Vec<f32>)>,
) -> Result<usize> {
let chunks: Vec<_> = rows
.chunks(chunk_rows::EMBEDDINGS)
.map(<[_]>::to_vec)
.collect();
self.low_chunked(chunks, |chunk, responder| {
LowPriCommand::UpsertEmbeddingChunk {
model: model.clone(),
chunk,
responder,
}
})
.await
}
pub async fn rebuild_fts(&self) -> Result<()> {
self.low(|responder| LowPriCommand::RebuildFts { responder })
.await
}
pub async fn write_analytics_annotations(&self, annotations: Vec<Annotation>) -> Result<usize> {
let chunks: Vec<_> = annotations
.chunks(chunk_rows::ANNOTATIONS)
.map(<[_]>::to_vec)
.collect();
self.low_chunked(chunks, |chunk, responder| {
LowPriCommand::WriteAnalyticsChunk { chunk, responder }
})
.await
}
pub async fn archive(&self, cutoff: &str) -> Result<ArchiveReport> {
let cutoff = timestamp::normalize(cutoff)?;
let archive_path = self.archive_path.clone();
self.low(|responder| LowPriCommand::Archive {
cutoff,
archive_path,
responder,
})
.await
}
pub async fn archive_windowed(
&self,
cutoff: &str,
window: std::time::Duration,
) -> Result<Vec<ArchiveReport>> {
let cutoff = timestamp::normalize(cutoff)?;
let boundaries = self.archive_boundaries(&cutoff, window).await?;
let mut reports = Vec::with_capacity(boundaries.len());
for boundary in boundaries {
let archive_path = self.archive_path.clone();
reports.push(
self.low(|responder| LowPriCommand::Archive {
cutoff: boundary,
archive_path,
responder,
})
.await?,
);
}
Ok(reports)
}
async fn archive_boundaries(
&self,
cutoff: &str,
window: std::time::Duration,
) -> Result<Vec<String>> {
let Some(oldest) = self.oldest_hot_stamp(cutoff).await? else {
return Ok(vec![cutoff.to_string()]);
};
let start = timestamp::parse(&oldest)?;
let end = timestamp::parse(cutoff)?;
let Ok(span) = end.duration_since(start) else {
return Ok(vec![cutoff.to_string()]);
};
if window.is_zero() {
return Err(DbError::ArchiveWindow {
window,
reason: "a zero-length window never advances past the first boundary".into(),
});
}
let sessions = span.as_nanos().div_ceil(window.as_nanos());
if sessions > MAX_ARCHIVE_SESSIONS as u128 {
return Err(DbError::ArchiveWindow {
window,
reason: format!(
"a span of {span:?} would need {sessions} sessions (limit \
{MAX_ARCHIVE_SESSIONS}); widen the window"
),
});
}
let mut boundaries = Vec::with_capacity(sessions as usize);
for k in 1..sessions {
boundaries.push(timestamp::format(start + window * k as u32));
}
boundaries.push(cutoff.to_string());
Ok(boundaries)
}
async fn oldest_hot_stamp(&self, cutoff: &str) -> Result<Option<String>> {
let mut oldest: Option<String> = None;
for table in ["links", "transaction_log"] {
let found: Option<String> = self
.read_conn
.query(
&format!("SELECT MIN(recorded_at) FROM {table} WHERE recorded_at < ?1"),
libsql::params![cutoff],
)
.await?
.next()
.await?
.and_then(|row| row.get(0).ok());
if let Some(found) = found {
if oldest.as_ref().is_none_or(|o| found < *o) {
oldest = Some(found);
}
}
}
Ok(oldest)
}
async fn high<T>(
&self,
make: impl FnOnce(oneshot::Sender<Result<T>>) -> HighPriCommand,
) -> Result<T> {
let (tx, rx) = oneshot::channel();
self.highpri_tx
.send(make(tx))
.await
.map_err(|_| DbError::WriterUnavailable)?;
rx.await.map_err(|_| DbError::WriterDroppedResponder)?
}
async fn low_chunked<C>(
&self,
chunks: Vec<C>,
make: impl Fn(C, oneshot::Sender<Result<usize>>) -> LowPriCommand,
) -> Result<usize> {
let mut written = 0usize;
for chunk in chunks {
let (tx, rx) = oneshot::channel();
self.lowpri_tx
.send(make(chunk, tx))
.await
.map_err(|_| DbError::WriterUnavailable)?;
written += rx.await.map_err(|_| DbError::WriterDroppedResponder)??;
}
Ok(written)
}
async fn low<T>(
&self,
make: impl FnOnce(oneshot::Sender<Result<T>>) -> LowPriCommand,
) -> Result<T> {
let (tx, rx) = oneshot::channel();
self.lowpri_tx
.send(make(tx))
.await
.map_err(|_| DbError::WriterUnavailable)?;
rx.await.map_err(|_| DbError::WriterDroppedResponder)?
}
pub async fn close(mut self) -> Result<()> {
if let Some(stop) = self.cadence_stop.take() {
let _ = stop.send(true);
}
if let Some(handle) = self.cadence.take() {
let _ = handle.await;
}
let (tx, rx) = oneshot::channel();
let _ = self
.highpri_tx
.send(HighPriCommand::Shutdown { responder: tx })
.await;
let _ = rx.await;
if let Some(handle) = self.writer.take() {
match handle.await {
Ok(res) => res?,
Err(e) => {
return Err(DbError::WriterStopped(format!(
"the write actor did not exit cleanly: {e}"
)))
}
}
}
let ts = self.clock.now();
let archive = self
.archive_path
.exists()
.then_some(self.archive_path.as_path());
snapshot::write_final(&self.read_conn, &self.snapshots_dir, &ts, archive).await?;
self.closed = true;
Ok(())
}
}
impl Drop for Database {
fn drop(&mut self) {
if !self.closed {
tracing::warn!(
"Database dropped without close(): the final snapshot was not written, \
so the next reconstruct folds from an older anchor, and the write \
actor's exit status was not checked. Prefer close().await."
);
}
}
}
fn normalize_all(edges: Vec<EdgeAssertion>) -> Result<Vec<EdgeAssertion>> {
edges.into_iter().map(EdgeAssertion::normalized).collect()
}
async fn configure(conn: libsql::Connection) -> Result<libsql::Connection> {
let _ = conn.query("PRAGMA journal_mode = WAL", ()).await?;
let _ = conn.query("PRAGMA busy_timeout = 5000", ()).await?;
conn.execute("PRAGMA synchronous = NORMAL", ()).await?;
conn.execute("PRAGMA foreign_keys = ON", ()).await?;
conn.execute("PRAGMA recursive_triggers = OFF", ()).await?;
Ok(conn)
}
fn derive_snapshots_dir(path: &Path) -> PathBuf {
let mut dir = path.to_path_buf();
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("macrame");
dir.set_file_name(format!("{stem}_snapshots"));
dir
}
fn derive_archive_path(path: &Path) -> PathBuf {
let mut archive = path.to_path_buf();
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("db");
archive.set_file_name(format!("{stem}_archive.{ext}"));
} else {
archive.set_extension("archive.db");
}
archive
}
async fn run_writer_actor(
conn: libsql::Connection,
clock: Arc<dyn Clock>,
mut highpri_rx: mpsc::Receiver<HighPriCommand>,
mut lowpri_rx: mpsc::Receiver<LowPriCommand>,
shared: Arc<ActorShared>,
) -> Result<()> {
loop {
shared
.metrics
.record_turn(highpri_rx.len(), lowpri_rx.len());
let ctl = tokio::select! {
biased;
Some(cmd) = highpri_rx.recv() => {
let turn = Turn::start(cmd.kind(), &shared);
cmd.execute(&conn, &*clock, &turn).await
}
Some(cmd) = lowpri_rx.recv() => {
let turn = Turn::start(cmd.kind(), &shared);
cmd.execute(&conn, &*clock, &turn).await
}
else => LoopCtl::Break,
};
if matches!(ctl, LoopCtl::Break) {
break;
}
}
Ok(())
}
struct Turn<'a> {
kind: crate::metrics::CommandKind,
timer: crate::metrics::HoldTimer,
shared: &'a ActorShared,
}
#[derive(Default)]
struct ActorShared {
metrics: crate::metrics::ActorMetrics,
archive_epoch: std::sync::atomic::AtomicU64,
}
impl<'a> Turn<'a> {
fn start(kind: crate::metrics::CommandKind, shared: &'a ActorShared) -> Self {
Self {
kind,
timer: crate::metrics::HoldTimer::start(),
shared,
}
}
fn epoch(&self) -> u64 {
self.shared
.archive_epoch
.load(std::sync::atomic::Ordering::Relaxed)
}
fn archive_committed(&self) {
self.shared
.archive_epoch
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
fn answer<T>(&self, responder: oneshot::Sender<Result<T>>, res: Result<T>) {
self.shared
.metrics
.record_hold(self.kind, self.timer.elapsed());
let _ = responder.send(res);
}
}
const INSERT_LINK: &str = "INSERT INTO links \
(source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)";
const UPSERT_CONCEPT: &str = "INSERT INTO concepts \
(id, title, content, embedding_model, valid_from, valid_to, recorded_at, retired) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) \
ON CONFLICT(id) DO UPDATE SET \
title = excluded.title, \
content = excluded.content, \
embedding_model = excluded.embedding_model, \
valid_from = excluded.valid_from, \
valid_to = excluded.valid_to, \
recorded_at = excluded.recorded_at, \
retired = excluded.retired";
fn concept_params<'a>(concept: &'a ConceptUpsert, stamp: &'a str) -> [libsql::Value; 8] {
[
concept.id.as_str().into(),
concept.title.as_str().into(),
concept.content.as_str().into(),
concept
.embedding_model
.as_deref()
.map_or(libsql::Value::Null, Into::into),
concept.valid_from.as_str().into(),
concept.valid_to.as_str().into(),
stamp.into(),
(concept.retired as i64).into(),
]
}
impl HighPriCommand {
fn kind(&self) -> crate::metrics::CommandKind {
use crate::metrics::CommandKind as K;
match self {
HighPriCommand::AssertEdge { .. } => K::AssertEdge,
HighPriCommand::RetireEdge { .. } => K::RetireEdge,
HighPriCommand::UpsertConcept { .. } => K::UpsertConcept,
HighPriCommand::WriteBulkAtomic { .. } => K::WriteBulkAtomic,
HighPriCommand::RebuildCurrent { .. } => K::RebuildCurrent,
HighPriCommand::RegisterModel { .. } => K::RegisterModel,
HighPriCommand::Shutdown { .. } => K::Shutdown,
}
}
async fn execute(
self,
conn: &libsql::Connection,
clock: &dyn Clock,
turn: &Turn<'_>,
) -> LoopCtl {
match self {
HighPriCommand::Shutdown { responder } => {
turn.answer(responder, Ok(()));
return LoopCtl::Break;
}
HighPriCommand::AssertEdge { edge, responder } => {
let stamp = clock.now();
if let Err(e) = reject_overlapping_interval(conn, &edge).await {
turn.answer(responder, Err(e));
return LoopCtl::Continue;
}
let res = match conn
.execute(
INSERT_LINK,
libsql::params![
edge.source.as_str(),
edge.target.as_str(),
edge.edge_type.as_str(),
edge.valid_from.as_str(),
edge.valid_to.as_str(),
edge.weight,
edge.properties.as_str(),
stamp.as_str()
],
)
.await
{
Ok(_) => Ok(()),
Err(e) => Err(classify(
conn,
e,
WriteOp::Edge {
source_id: &edge.source,
target_id: &edge.target,
edge_type: &edge.edge_type,
},
)
.await),
};
turn.answer(responder, res);
}
HighPriCommand::RetireEdge {
source,
target,
edge_type,
valid_from,
valid_to,
responder,
} => {
let stamp = clock.now();
let res = retire_edge(
conn,
&source,
&target,
&edge_type,
&valid_from,
&valid_to,
&stamp,
)
.await;
turn.answer(responder, res);
}
HighPriCommand::UpsertConcept { concept, responder } => {
let stamp = clock.now();
let res = upsert_concept(conn, &concept, &stamp).await;
turn.answer(responder, res);
}
HighPriCommand::WriteBulkAtomic { edges, responder } => {
let stamp = clock.now();
let res = write_edges_atomic(conn, &edges, &stamp).await;
turn.answer(responder, res);
}
HighPriCommand::RebuildCurrent { responder } => {
turn.answer(responder, rebuild_current(conn).await);
}
HighPriCommand::RegisterModel {
model,
dim,
responder,
} => {
turn.answer(
responder,
crate::vector::register_model(conn, &model, dim).await,
);
}
}
LoopCtl::Continue
}
}
impl LowPriCommand {
fn kind(&self) -> crate::metrics::CommandKind {
use crate::metrics::CommandKind as K;
match self {
LowPriCommand::WriteConceptsChunk { .. } => K::WriteConceptsChunk,
LowPriCommand::WriteAnalyticsChunk { .. } => K::WriteAnalyticsChunk,
LowPriCommand::UpsertEmbeddingChunk { .. } => K::UpsertEmbeddingChunk,
LowPriCommand::BulkImportChunk { .. } => K::BulkImportChunk,
LowPriCommand::Archive { .. } => K::Archive,
LowPriCommand::RebuildFts { .. } => K::RebuildFts,
LowPriCommand::ShadowRebuild { .. } => K::ShadowRebuild,
}
}
async fn execute(
self,
conn: &libsql::Connection,
clock: &dyn Clock,
turn: &Turn<'_>,
) -> LoopCtl {
match self {
LowPriCommand::BulkImportChunk { chunk, responder } => {
let stamp = clock.now();
turn.answer(responder, write_edges_atomic(conn, &chunk, &stamp).await);
}
LowPriCommand::WriteConceptsChunk { chunk, responder } => {
let stamp = clock.now();
turn.answer(responder, write_concepts_atomic(conn, &chunk, &stamp).await);
}
LowPriCommand::WriteAnalyticsChunk { chunk, responder } => {
let stamp = clock.now();
turn.answer(
responder,
write_annotations_atomic(conn, &chunk, &stamp).await,
);
}
LowPriCommand::UpsertEmbeddingChunk {
model,
chunk,
responder,
} => {
turn.answer(
responder,
crate::vector::search::upsert_embedding_chunk(conn, &model, &chunk).await,
);
}
LowPriCommand::Archive {
cutoff,
archive_path,
responder,
} => {
let archived_at = clock.now();
let res = archive(conn, &cutoff, &archived_at, &archive_path).await;
if res.is_ok() {
turn.archive_committed();
}
turn.answer(responder, res);
}
LowPriCommand::ShadowRebuild { step, responder } => {
use crate::integrity::{shadow, ShadowOutcome, ShadowStep};
let res = match step {
ShadowStep::Begin => {
shadow::begin(conn)
.await
.map(|build_start| ShadowOutcome::Started {
build_start,
epoch: turn.epoch(),
})
}
ShadowStep::Fill { after } => shadow::fill_chunk(conn, after.as_deref())
.await
.map(|last| ShadowOutcome::Filled { last }),
ShadowStep::Swap { build_start, epoch } => {
shadow::swap(conn, &build_start, epoch, turn.epoch())
.await
.map(|rows| ShadowOutcome::Swapped { rows })
}
};
turn.answer(responder, res);
}
LowPriCommand::RebuildFts { responder } => {
let res = conn
.execute(crate::schema::ddl::REBUILD_CONCEPTS_FTS, ())
.await
.map(|_| ())
.map_err(Into::into);
turn.answer(responder, res);
}
}
LoopCtl::Continue
}
}
async fn retire_edge(
conn: &libsql::Connection,
source: &str,
target: &str,
edge_type: &str,
valid_from: &str,
valid_to: &str,
stamp: &str,
) -> Result<()> {
let affected = conn
.execute(
"INSERT INTO links \
(source_id, target_id, edge_type, valid_from, valid_to, weight, properties, recorded_at) \
SELECT source_id, target_id, edge_type, valid_from, ?5, weight, properties, ?6 \
FROM links_current \
WHERE source_id = ?1 AND target_id = ?2 AND edge_type = ?3 AND valid_from = ?4",
libsql::params![source, target, edge_type, valid_from, valid_to, stamp],
)
.await
.map_err(DbError::Engine)?;
if affected == 0 {
return Err(DbError::NotFound(format!(
"{source} -> {target} ({edge_type}) at {valid_from}"
)));
}
Ok(())
}
async fn upsert_concept(
conn: &libsql::Connection,
concept: &ConceptUpsert,
stamp: &str,
) -> Result<()> {
let res = conn
.execute(UPSERT_CONCEPT, concept_params(concept, stamp))
.await;
match res {
Ok(_) => Ok(()),
Err(e) => Err(classify(
conn,
e,
WriteOp::Concept {
id: &concept.id,
recorded_at: stamp,
},
)
.await),
}
}
const OVERLAP_CANDIDATES: &str = "SELECT valid_from, valid_to FROM links_current \
WHERE source_id = ?1 AND target_id = ?2 AND edge_type = ?3 \
AND valid_from <> ?4";
fn defer_to_single_open(proposed: &Interval, existing: &Interval) -> bool {
proposed.is_open() && existing.is_open()
}
async fn reject_overlapping_interval(
conn: &libsql::Connection,
edge: &EdgeAssertion,
) -> Result<()> {
let stmt = conn.prepare(OVERLAP_CANDIDATES).await?;
check_prepared(&stmt, edge).await
}
async fn check_prepared(stmt: &libsql::Statement, edge: &EdgeAssertion) -> Result<()> {
let proposed = Interval::new(edge.valid_from.clone(), edge.valid_to.clone());
stmt.reset();
let mut rows = stmt
.query(libsql::params![
edge.source.as_str(),
edge.target.as_str(),
edge.edge_type.as_str(),
edge.valid_from.as_str()
])
.await?;
while let Some(row) = rows.next().await? {
let existing = Interval::new(row.get::<String>(0)?, row.get::<String>(1)?);
if defer_to_single_open(&proposed, &existing) {
continue;
}
if proposed.overlaps(&existing) {
return Err(DbError::OverlappingInterval {
overlap: Box::new(crate::error::Overlap {
source_id: edge.source.clone(),
target_id: edge.target.clone(),
edge_type: edge.edge_type.clone(),
valid_from: edge.valid_from.clone(),
valid_to: edge.valid_to.clone(),
existing_from: existing.valid_from,
existing_to: existing.valid_to,
}),
});
}
}
Ok(())
}
fn reject_overlaps_within(edges: &[EdgeAssertion]) -> Result<()> {
for (i, a) in edges.iter().enumerate() {
let ia = Interval::new(a.valid_from.clone(), a.valid_to.clone());
for b in &edges[i + 1..] {
if a.source != b.source || a.target != b.target || a.edge_type != b.edge_type {
continue;
}
if a.valid_from == b.valid_from {
continue;
}
let ib = Interval::new(b.valid_from.clone(), b.valid_to.clone());
if defer_to_single_open(&ia, &ib) {
continue;
}
if ia.overlaps(&ib) {
return Err(DbError::OverlappingInterval {
overlap: Box::new(crate::error::Overlap {
source_id: a.source.clone(),
target_id: a.target.clone(),
edge_type: a.edge_type.clone(),
valid_from: a.valid_from.clone(),
valid_to: a.valid_to.clone(),
existing_from: ib.valid_from,
existing_to: ib.valid_to,
}),
});
}
}
}
Ok(())
}
async fn write_edges_atomic(
conn: &libsql::Connection,
edges: &[EdgeAssertion],
stamp: &str,
) -> Result<usize> {
if edges.is_empty() {
return Ok(0);
}
reject_overlaps_within(edges)?;
let tx = conn
.transaction_with_behavior(libsql::TransactionBehavior::Immediate)
.await?;
let guard = tx.prepare(OVERLAP_CANDIDATES).await?;
for edge in edges {
if let Err(e) = check_prepared(&guard, edge).await {
drop(guard);
let _ = tx.rollback().await;
return Err(e);
}
}
drop(guard);
let stmt = tx.prepare(INSERT_LINK).await?;
for edge in edges {
stmt.reset();
let res = stmt
.execute(libsql::params![
edge.source.as_str(),
edge.target.as_str(),
edge.edge_type.as_str(),
edge.valid_from.as_str(),
edge.valid_to.as_str(),
edge.weight,
edge.properties.as_str(),
stamp
])
.await;
if let Err(e) = res {
let typed = classify(
&tx,
e,
WriteOp::Edge {
source_id: &edge.source,
target_id: &edge.target,
edge_type: &edge.edge_type,
},
)
.await;
drop(stmt);
let _ = tx.rollback().await;
return Err(typed);
}
}
drop(stmt);
tx.commit().await?;
Ok(edges.len())
}
async fn write_annotations_atomic(
conn: &libsql::Connection,
annotations: &[Annotation],
stamp: &str,
) -> Result<usize> {
if annotations.is_empty() {
return Ok(0);
}
let tx = conn
.transaction_with_behavior(libsql::TransactionBehavior::Immediate)
.await?;
let stmt = tx
.prepare(
"INSERT INTO analytics_annotations (concept_id, label, value, computed_at) \
VALUES (?1, ?2, ?3, ?4) \
ON CONFLICT(concept_id, label) DO UPDATE SET \
value = excluded.value, computed_at = excluded.computed_at",
)
.await?;
for a in annotations {
stmt.reset();
let res = stmt
.execute(libsql::params![
a.concept_id.as_str(),
a.label.as_str(),
a.value.as_str(),
stamp
])
.await;
if let Err(e) = res {
drop(stmt);
let _ = tx.rollback().await;
return Err(DbError::Engine(e));
}
}
drop(stmt);
tx.commit().await?;
Ok(annotations.len())
}
async fn write_concepts_atomic(
conn: &libsql::Connection,
concepts: &[ConceptUpsert],
stamp: &str,
) -> Result<usize> {
if concepts.is_empty() {
return Ok(0);
}
let tx = conn
.transaction_with_behavior(libsql::TransactionBehavior::Immediate)
.await?;
let stmt = tx.prepare(UPSERT_CONCEPT).await?;
for concept in concepts {
stmt.reset();
let res = stmt.execute(concept_params(concept, stamp)).await;
if let Err(e) = res {
let typed = classify(
&tx,
e,
WriteOp::Concept {
id: &concept.id,
recorded_at: stamp,
},
)
.await;
drop(stmt);
let _ = tx.rollback().await;
return Err(typed);
}
}
drop(stmt);
tx.commit().await?;
Ok(concepts.len())
}
#[cfg(test)]
mod tests {
use super::*;
fn edge(target: &str, micros: usize) -> EdgeAssertion {
EdgeAssertion::new("src", target, "LINKS")
.valid_from(format!("2026-01-01T00:00:00.{micros:06}Z"))
.valid_to(format!("2026-01-01T00:00:00.{:06}Z", micros + 1))
}
#[test]
fn two_batches_of_one_size_are_not_predicted_alike() {
const N: usize = 20_000;
let fanout: Vec<_> = (0..N).map(|i| edge(&format!("t{i:07}"), i)).collect();
let history: Vec<_> = (0..N).map(|i| edge("t0", i)).collect();
let (a, b) = (estimated_bulk_hold(&fanout), estimated_bulk_hold(&history));
assert!(
b > a * 5,
"the guard's expensive path is 16x dearer per pair and this batch \
takes it on every pair, but the estimates are {a:?} and {b:?}"
);
}
#[test]
fn the_estimate_matches_what_was_measured() {
const N: usize = 20_000;
let fanout: Vec<_> = (0..N).map(|i| edge(&format!("t{i:07}"), i)).collect();
let history: Vec<_> = (0..N).map(|i| edge("t0", i)).collect();
for (batch, measured_ms, label) in
[(fanout, 2_618u128, "fanout"), (history, 18_057, "history")]
{
let predicted = estimated_bulk_hold(&batch).as_millis();
let ratio = predicted as f64 / measured_ms as f64;
assert!(
(0.8..1.25).contains(&ratio),
"{label}: predicted {predicted} ms against a measured \
{measured_ms} ms ({ratio:.2}x). Re-run \
examples/bulk_atomic_diag.rs before changing the coefficients."
);
}
}
#[test]
fn a_batch_too_small_to_have_pairs_still_estimates() {
assert_eq!(estimated_bulk_hold(&[]), std::time::Duration::ZERO);
let one = [edge("t0", 0)];
assert_eq!(
estimated_bulk_hold(&one),
std::time::Duration::from_nanos(73_000)
);
}
#[test]
fn the_warning_threshold_is_not_the_chunk_budget() {
assert!(BULK_ATOMIC_WARN_HOLD > CHUNK_BUDGET * 10);
}
}