use nodedb_types::document::Document;
use nodedb_types::error::{NodeDbError, NodeDbResult};
use crate::engine::document::history::ops::{
is_bitemporal, versioned_get_as_of, versioned_get_current, versioned_put, versioned_tombstone,
};
use crate::engine::document::history::value::DecodedVersion;
use crate::nodedb::LockExt;
use crate::nodedb::NodeDbLite;
use crate::nodedb::convert::{document_to_msgpack, loro_value_to_document, value_to_loro};
use crate::runtime::{monotonic_millis_i64, now_millis_i64};
use crate::storage::engine::StorageEngine;
impl<S: StorageEngine> NodeDbLite<S> {
pub(super) async fn document_get_impl(
&self,
collection: &str,
id: &str,
) -> NodeDbResult<Option<Document>> {
if is_bitemporal(&*self.storage, collection)
.await
.map_err(NodeDbError::storage)?
{
let version = versioned_get_current(&*self.storage, collection, id)
.await
.map_err(NodeDbError::storage)?;
return Ok(version.map(|v| decoded_version_to_document(id, &v)));
}
let crdt = self.crdt.lock_or_recover();
let Some(value) = crdt.read(collection, id) else {
return Ok(None);
};
Ok(Some(loro_value_to_document(id, &value)))
}
pub(super) async fn document_put_impl(
&self,
collection: &str,
doc: Document,
) -> NodeDbResult<()> {
let guard = self.fts_state.admit_mutation().await;
let result = async {
if self.governor.worst_engine_pressure() == nodedb_mem::PressureLevel::Emergency {
return Err(NodeDbError::storage(
crate::error::LiteError::Backpressure {
detail: "document put rejected: memory governor is at Emergency pressure"
.into(),
},
));
}
let doc_id = if doc.id.is_empty() {
nodedb_types::id_gen::uuid_v7()
} else {
doc.id.clone()
};
let bitemporal = is_bitemporal(&*self.storage, collection)
.await
.map_err(NodeDbError::storage)?;
let _index_build = self.hold_bitemporal_build(bitemporal).await;
self.query_engine.indexes.revive(collection, &doc_id);
{
let mut crdt = self.crdt.lock_or_recover();
let fields: Vec<(&str, loro::LoroValue)> = doc
.fields
.iter()
.map(|(k, v)| (k.as_str(), value_to_loro(v)))
.collect();
let mutation_id = crdt
.upsert(collection, &doc_id, &fields)
.map_err(NodeDbError::from)?;
if !self.should_sync_doc(collection, &doc.fields) {
crdt.drop_pending(mutation_id);
}
}
if bitemporal {
let now_ms = monotonic_millis_i64();
let body = document_to_msgpack(&doc);
versioned_put(
&*self.storage,
collection,
&doc_id,
&body,
now_ms,
Some(now_millis_i64()),
None,
)
.await
.map_err(NodeDbError::storage)?;
}
self.index_document_text(collection, &doc_id, &doc.fields)?;
self.index_document_sparse(collection, &doc_id, &doc.fields);
Ok(())
}
.await;
guard.finish(result)
}
pub(super) async fn document_delete_impl(
&self,
collection: &str,
id: &str,
) -> NodeDbResult<()> {
let guard = self.fts_state.admit_mutation().await;
let result = async {
if is_bitemporal(&*self.storage, collection)
.await
.map_err(NodeDbError::storage)?
{
let _index_build = self.hold_bitemporal_build(true).await;
let now_ms = monotonic_millis_i64();
versioned_tombstone(
&*self.storage,
collection,
id,
now_ms,
Some(now_millis_i64()),
)
.await
.map_err(NodeDbError::storage)?;
self.query_engine.indexes.tombstone(collection, [id]);
self.remove_document_text(collection, id)?;
self.remove_document_sparse(collection, id);
return Ok(());
}
let mut crdt = self.crdt.lock_or_recover();
crdt.delete(collection, id).map_err(NodeDbError::storage)?;
drop(crdt);
self.remove_document_text(collection, id)?;
self.remove_document_sparse(collection, id);
Ok(())
}
.await;
guard.finish(result)
}
pub(super) async fn document_get_as_of_impl(
&self,
collection: &str,
id: &str,
as_of_ms: Option<i64>,
valid_time_ms: Option<i64>,
) -> NodeDbResult<Option<Document>> {
if !is_bitemporal(&*self.storage, collection)
.await
.map_err(NodeDbError::storage)?
{
return Err(NodeDbError::storage(
"document_get_as_of requires a collection created WITH (bitemporal=true)",
));
}
let sys_as_of = as_of_ms.unwrap_or(i64::MAX);
let version = versioned_get_as_of(&*self.storage, collection, id, sys_as_of, valid_time_ms)
.await
.map_err(NodeDbError::storage)?;
Ok(version.map(|v| decoded_version_to_document(id, &v)))
}
pub(super) async fn document_put_with_valid_time_impl(
&self,
collection: &str,
doc: Document,
valid_from_ms: Option<i64>,
valid_until_ms: Option<i64>,
) -> NodeDbResult<()> {
let guard = self.fts_state.admit_mutation().await;
let result = async {
if !is_bitemporal(&*self.storage, collection)
.await
.map_err(NodeDbError::storage)?
{
return Err(NodeDbError::storage(
"document_put_with_valid_time requires a collection created WITH (bitemporal=true)",
));
}
let doc_id = if doc.id.is_empty() {
nodedb_types::id_gen::uuid_v7()
} else {
doc.id.clone()
};
let _index_build = self.hold_bitemporal_build(true).await;
self.query_engine.indexes.revive(collection, &doc_id);
{
let mut crdt = self.crdt.lock_or_recover();
let fields: Vec<(&str, loro::LoroValue)> = doc
.fields
.iter()
.map(|(k, v)| (k.as_str(), value_to_loro(v)))
.collect();
crdt.upsert(collection, &doc_id, &fields)
.map_err(NodeDbError::from)?;
}
let now_ms = monotonic_millis_i64();
let body = document_to_msgpack(&doc);
versioned_put(
&*self.storage,
collection,
&doc_id,
&body,
now_ms,
valid_from_ms.or_else(|| Some(now_millis_i64())),
valid_until_ms,
)
.await
.map_err(NodeDbError::storage)?;
self.index_document_text(collection, &doc_id, &doc.fields)?;
self.index_document_sparse(collection, &doc_id, &doc.fields);
Ok(())
}.await;
guard.finish(result)
}
}
impl<S: StorageEngine> NodeDbLite<S> {
pub(super) async fn hold_bitemporal_build(
&self,
bitemporal: bool,
) -> Option<tokio::sync::RwLockReadGuard<'_, ()>> {
if bitemporal {
Some(self.query_engine.indexes.bitemporal_build.read().await)
} else {
None
}
}
}
fn decoded_version_to_document(id: &str, version: &DecodedVersion) -> Document {
use nodedb_types::value::Value;
let mut doc = Document::new(id);
if version.body.is_empty() {
return doc;
}
if let Ok(Value::Object(fields)) = nodedb_types::json_msgpack::value_from_msgpack(&version.body)
{
for (k, v) in fields {
doc.set(k, v);
}
}
doc
}
#[cfg(test)]
mod tests {
use crate::{LiteConfig, NodeDbLite, PagedbStorageMem};
use nodedb_client::NodeDb;
use nodedb_types::document::Document;
use nodedb_types::value::Value;
#[tokio::test]
async fn document_write_waits_for_admission_and_nested_sql_writes_finish() {
let storage = PagedbStorageMem::open_in_memory().await.unwrap();
let db = NodeDbLite::open_with_config(
storage,
LiteConfig {
auto_flush_ms: 0,
sync_enabled: false,
..LiteConfig::default()
},
)
.await
.unwrap();
let mut doc = Document::new("a");
doc.set("body", Value::String("coordinated source".into()));
let permit = db.fts_state.admit_exclusive().await;
let mut write = Box::pin(db.document_put("docs", doc));
assert!(
tokio::time::timeout(std::time::Duration::from_millis(10), &mut write)
.await
.is_err()
);
assert!(db.document_get("docs", "a").await.unwrap().is_none());
drop(permit);
write.await.unwrap();
assert!(db.document_get("docs", "a").await.unwrap().is_some());
db.execute_sql("CREATE COLLECTION copied", &[])
.await
.unwrap();
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
db.execute_sql("INSERT INTO copied SELECT * FROM docs", &[]),
)
.await
.unwrap()
.unwrap();
assert_eq!(result.rows_affected, 1);
}
}