use async_trait::async_trait;
use fail_parallel::fail_point;
use futures::stream::BoxStream;
use futures::{FutureExt, StreamExt};
use log::warn;
use std::sync::Arc;
use std::time::Duration;
use tracing::instrument;
use std::collections::BTreeSet;
use crate::config::WriteOptions;
use crate::db_state::DbState;
use crate::db_transaction::DbTransaction;
use crate::dispatcher::MessageHandler;
use crate::mem_table::KVTable;
use crate::types::RowEntry;
use crate::utils::WatchableOnceCellReader;
use crate::wal::{FlushResultFuture, WalWriter};
use crate::{batch::WriteBatch, db::DbInner, db::WriteHandle, error::SlateDBError};
use bytes::Bytes;
use parking_lot::RwLockWriteGuard;
use slatedb_common::clock::SystemClock;
use tokio::sync::oneshot;
pub(crate) const WRITE_BATCH_TASK_NAME: &str = "writer";
pub(crate) type WriteBatchResult = Result<WriteHandle, SlateDBError>;
#[allow(clippy::large_enum_variant)]
pub(crate) enum BatchWriterMessage {
WriteBatch(WriteBatchRequest),
Flush(BatchWriterFlush),
}
pub(crate) struct BatchWriterFlush {
freeze_memtable: bool,
done: oneshot::Sender<Result<FlushResultFuture, SlateDBError>>,
}
pub(crate) struct WriteBatchRequest {
pub(crate) batch: WriteBatch,
pub(crate) options: WriteOptions,
pub(crate) done: oneshot::Sender<WriteBatchResult>,
pub(crate) txn: Option<DbTransaction>,
}
impl std::fmt::Debug for BatchWriterMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BatchWriterMessage::WriteBatch(WriteBatchRequest { batch, options, .. }) => f
.debug_struct("WriteBatch")
.field("batch", batch)
.field("options", options)
.finish(),
BatchWriterMessage::Flush(BatchWriterFlush {
freeze_memtable, ..
}) => f
.debug_struct("Flush")
.field("freeze_memtable", freeze_memtable)
.finish(),
}
}
}
pub(crate) struct WriteBatchEventHandler {
db_inner: Arc<DbInner>,
is_first_write: bool,
wal_writer: Option<Box<dyn WalWriter>>,
}
impl WriteBatchEventHandler {
pub(crate) fn new(db_inner: Arc<DbInner>, wal_writer: Option<Box<dyn WalWriter>>) -> Self {
Self {
db_inner,
is_first_write: true,
wal_writer,
}
}
}
#[async_trait]
impl MessageHandler<BatchWriterMessage> for WriteBatchEventHandler {
async fn handle(&mut self, message: BatchWriterMessage) -> Result<(), SlateDBError> {
match message {
BatchWriterMessage::WriteBatch(WriteBatchRequest {
batch,
options,
done,
txn,
}) => {
let result = self
.db_inner
.write_batch(
batch,
&options,
txn.as_ref(),
self.wal_writer.as_mut(),
self.is_first_write,
)
.await;
self.is_first_write = false;
match result {
Ok(write_result) => {
let _ = done.send(write_result);
Ok(())
}
Err(error) => {
let _ = done.send(Err(error.clone()));
Err(error)
}
}
}
BatchWriterMessage::Flush(flush_msg) => {
let BatchWriterFlush {
freeze_memtable,
done,
} = flush_msg;
let result = self
.db_inner
.flush_batch_writer(freeze_memtable, self.wal_writer.as_mut())
.await;
match result {
Ok(flush_result) => {
let _ = done.send(Ok(flush_result));
Ok(())
}
Err(error) => {
let _ = done.send(Err(error.clone()));
Err(error)
}
}
}
}
}
async fn cleanup(
&mut self,
mut messages: BoxStream<'async_trait, BatchWriterMessage>,
result: Result<(), SlateDBError>,
) -> Result<(), SlateDBError> {
let error = result.clone().err().unwrap_or(SlateDBError::Closed);
while let Some(msg) = messages.next().await {
match msg {
BatchWriterMessage::WriteBatch(req) => {
let _ = req.done.send(Err(error.clone()));
}
BatchWriterMessage::Flush(flush_msg) => {
let BatchWriterFlush {
freeze_memtable: _,
done,
} = flush_msg;
let _ = done.send(Err(error.clone()));
}
}
}
if let Some(wal_writer) = self.wal_writer.as_mut() {
wal_writer.close().await?;
}
Ok(())
}
}
impl DbInner {
#[allow(clippy::panic)]
#[instrument(level = "trace", skip_all, fields(batch_size = batch.op_count()))]
async fn write_batch(
&self,
batch: WriteBatch,
options: &WriteOptions,
txn: Option<&DbTransaction>,
wal_writer: Option<&mut Box<dyn WalWriter>>,
is_first_write: bool,
) -> Result<WriteBatchResult, SlateDBError> {
let _options = options;
#[cfg(not(dst))]
let now = self.mono_clock.now().await?;
#[cfg(dst)]
let now = options.now;
let commit_seq = if options.seqnum > 0 {
let current = self.oracle.last_seq();
if options.seqnum <= current {
return Ok(Err(SlateDBError::InvalidSequenceNumber {
provided: options.seqnum,
current,
}));
}
self.oracle.advance_last_seq(options.seqnum);
options.seqnum
} else {
self.oracle.next_seq()
};
if let Some(txn) = txn {
if self.txn_manager.check_has_conflict(&txn.id()) {
return Ok(Err(SlateDBError::TransactionConflict));
}
}
let (entries, touched_segments, entries_size) = match batch
.extract_entries(
commit_seq,
now,
self.settings.default_ttl,
self.flush_merge_operator.clone(),
self.segment_extractor.as_deref(),
)
.await
{
Ok(extracted) => extracted,
Err(error) => return Ok(Err(error)),
};
if let Err(error) = self.validate_segment_antichain(&touched_segments) {
return Ok(Err(error));
}
if let Some(wal_writer) = wal_writer {
assert!(self.wal_enabled);
wal_writer.append(&entries).await?;
self.write_entries_to_memtable(entries, touched_segments);
} else {
assert!(!self.wal_enabled);
let watcher = self.write_entries_to_memtable(entries, touched_segments);
if is_first_write && options.await_durable {
let this_watcher = watcher.clone();
let this_clock = self.system_clock.clone();
tokio::spawn(async move {
monitor_first_write(this_watcher, this_clock).await;
});
}
};
self.db_stats.memtable_write_bytes.increment(entries_size);
fail_point!(
Arc::clone(&self.fp_registry),
"write-batch-pre-commit",
|_| { Err(SlateDBError::from(std::io::Error::other("oops"))) }
);
if let Some(txn) = txn {
self.txn_manager
.track_recent_committed_txn(&txn.id(), commit_seq);
} else {
let write_keys = batch.keys();
self.txn_manager
.track_recent_committed_write_batch(&write_keys, commit_seq);
}
fail_point!(
Arc::clone(&self.fp_registry),
"write-batch-post-commit",
|_| { Err(SlateDBError::from(std::io::Error::other("oops"))) }
);
self.record_memtable_sequence(commit_seq);
self.maybe_freeze_current_memtable()?;
let write_handle = WriteHandle::new(commit_seq, now);
Ok(Ok(write_handle))
}
fn maybe_freeze_current_memtable(&self) -> Result<(), SlateDBError> {
let replay_after_wal_id = self.wal_observer.status()?.last_flushed_wal_id;
let mut guard = self.state.write();
let meta = guard.memtable().metadata();
let last_freeze_wal_id = guard
.state()
.imm_memtable
.front()
.map(|imm| imm.recent_flushed_wal_id())
.unwrap_or(guard.state().core().replay_after_wal_id);
let l0_sst_size_est = self
.table_store
.estimate_encoded_size_compacted(meta.entry_num, meta.entries_size_in_bytes);
let wal_id_gap = replay_after_wal_id
.checked_sub(last_freeze_wal_id)
.ok_or_else(|| SlateDBError::InvalidDBState)?;
if wal_id_gap < self.settings.max_wal_flushes_before_l0_flush
&& l0_sst_size_est < self.settings.l0_sst_size_bytes
{
return Ok(());
}
self.freeze_current_memtable_with_state_guard(&mut guard, replay_after_wal_id);
Ok(())
}
async fn flush_batch_writer(
&self,
freeze_memtable: bool,
wal_writer: Option<&mut Box<dyn WalWriter>>,
) -> Result<FlushResultFuture, SlateDBError> {
let flush_rx = if let Some(wal_writer) = wal_writer {
wal_writer.flush().await?
} else {
async { Ok(()) }.boxed()
};
if freeze_memtable {
let replay_after_wal_id = self.wal_observer.status()?.last_flushed_wal_id;
let mut guard = self.state.write();
self.freeze_current_memtable_with_state_guard(&mut guard, replay_after_wal_id);
}
Ok(flush_rx)
}
pub(crate) fn freeze_current_memtable_with_state_guard(
&self,
guard: &mut RwLockWriteGuard<'_, DbState>,
replay_after_wal_id: u64,
) {
if guard.memtable().is_empty() {
return;
}
guard.freeze_memtable(replay_after_wal_id);
let _ = self.memtable_flusher().notify_memtable_frozen();
}
#[instrument(level = "trace", skip_all, err(level = tracing::Level::DEBUG))]
pub(crate) async fn request_batch_writer_flush(
&self,
freeze_memtable: bool,
) -> Result<(), SlateDBError> {
let (done, rx) = tokio::sync::oneshot::channel();
self.write_notifier
.send(BatchWriterMessage::Flush(BatchWriterFlush {
freeze_memtable,
done,
}))?;
Ok(rx.await??.await?)
}
fn validate_segment_antichain(
&self,
batch_prefixes: &BTreeSet<Bytes>,
) -> Result<(), SlateDBError> {
if batch_prefixes.is_empty() {
return Ok(());
}
check_batch_antichain(batch_prefixes)?;
let mut remaining = batch_prefixes.clone();
let guard = self.state.read();
let memtable = guard.memtable().table();
check_segment_prefix_antichain(&mut remaining, memtable)?;
if remaining.is_empty() {
return Ok(());
}
let cow = guard.state();
for imm in cow.imm_memtable.iter() {
check_segment_prefix_antichain(&mut remaining, &imm.table())?;
if remaining.is_empty() {
return Ok(());
}
}
let core = cow.core();
for c in &remaining {
core.check_segment_prefix_antichain(c.as_ref())?;
}
Ok(())
}
fn write_entries_to_memtable(
&self,
entries: Vec<RowEntry>,
touched_segments: BTreeSet<Bytes>,
) -> WatchableOnceCellReader<Result<(), SlateDBError>> {
let guard = self.state.read();
let memtable = guard.memtable();
self.status_manager.add_memtable_segments(&touched_segments);
memtable.record_touched_segments(touched_segments.clone());
entries.into_iter().for_each(|entry| memtable.put(entry));
memtable.table().durable_watcher()
}
fn record_memtable_sequence(&self, seq: u64) {
let ts = self.system_clock.now();
let guard = self.state.read();
guard.memtable().record_sequence(seq, ts);
}
}
fn check_batch_antichain(prefixes: &BTreeSet<Bytes>) -> Result<(), SlateDBError> {
let mut prev: Option<&Bytes> = None;
for cur in prefixes {
if let Some(p) = prev {
if cur.starts_with(p.as_ref()) {
return Err(SlateDBError::InvalidSegmentPrefix {
prefix: cur.clone(),
conflict: p.clone(),
});
}
}
prev = Some(cur);
}
Ok(())
}
fn check_segment_prefix_antichain(
remaining: &mut BTreeSet<Bytes>,
table: &KVTable,
) -> Result<(), SlateDBError> {
let mut to_remove: Vec<Bytes> = Vec::new();
for c in remaining.iter() {
if table.ensure_valid_segment(c)? {
to_remove.push(c.clone());
}
}
for k in to_remove {
remaining.remove(&k);
}
Ok(())
}
async fn monitor_first_write(
mut watcher: WatchableOnceCellReader<Result<(), SlateDBError>>,
system_clock: Arc<dyn SystemClock>,
) {
tokio::select! {
_ = watcher.await_value() => {}
_ = system_clock.sleep(Duration::from_secs(5)) => {
warn!("First write not durable after 5 seconds and WAL is disabled. \
SlateDB does not automatically flush memtables until `l0_sst_size_bytes` \
is reached. If writer is single threaded or has low throughput, the \
applications must call `flush` to ensure durability in a timely manner.");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object_store::memory::InMemory;
use crate::wal::test_utils::FakeWalWriter;
use crate::wal::{WalError, WalObserver, WalStatus};
use crate::Db;
enum FailingWalOperation {
Append,
Flush,
}
struct FailingWalWriter {
inner: FakeWalWriter,
operation: FailingWalOperation,
}
impl FailingWalWriter {
fn new(operation: FailingWalOperation) -> Self {
Self {
inner: FakeWalWriter::new(0),
operation,
}
}
}
#[async_trait]
impl WalWriter for FailingWalWriter {
async fn append(&mut self, write_batch: &[RowEntry]) -> Result<(), WalError> {
if matches!(self.operation, FailingWalOperation::Append) {
return Err(WalError::Fenced);
}
self.inner.append(write_batch).await
}
async fn flush(&mut self) -> Result<FlushResultFuture, WalError> {
if matches!(self.operation, FailingWalOperation::Flush) {
return Err(WalError::Fenced);
}
self.inner.flush().await
}
fn observer(&self) -> Box<dyn WalObserver> {
self.inner.observer()
}
fn status(&self) -> Result<WalStatus, WalStatus> {
self.inner.status()
}
async fn close(&mut self) -> Result<(), WalError> {
self.inner.close().await
}
}
fn test_message(
batch: WriteBatch,
options: WriteOptions,
) -> (
BatchWriterMessage,
tokio::sync::oneshot::Receiver<WriteBatchResult>,
) {
let (done, rx) = tokio::sync::oneshot::channel();
(
BatchWriterMessage::WriteBatch(WriteBatchRequest {
batch,
options,
done,
txn: None,
}),
rx,
)
}
#[tokio::test]
async fn test_is_first_write_set_false_after_first_write() {
let object_store = Arc::new(InMemory::new());
let db = Db::open(
"/tmp/test_is_first_write_set_false_after_first_write",
object_store,
)
.await
.unwrap();
let wal_writer = Box::new(FakeWalWriter::new(0));
let mut handler = WriteBatchEventHandler::new(db.inner.clone(), Some(wal_writer));
assert!(handler.is_first_write);
let mut batch = WriteBatch::new();
batch.put(b"key", b"value");
let (msg, done_rx) = test_message(batch, WriteOptions::default());
handler.handle(msg).await.unwrap();
let result = done_rx.await.unwrap();
assert!(result.is_ok());
assert!(!handler.is_first_write);
}
#[tokio::test]
async fn test_append_error_notifies_caller_and_fails_handler() {
let object_store = Arc::new(InMemory::new());
let db = Db::open(
"/tmp/test_append_error_notifies_caller_and_fails_handler",
object_store,
)
.await
.unwrap();
let wal_writer = Box::new(FailingWalWriter::new(FailingWalOperation::Append));
let mut handler = WriteBatchEventHandler::new(db.inner.clone(), Some(wal_writer));
let mut batch = WriteBatch::new();
batch.put(b"key", b"value");
let (msg, done_rx) = test_message(batch, WriteOptions::default());
let handler_error = handler.handle(msg).await.unwrap_err();
assert!(matches!(handler_error, SlateDBError::Fenced));
let caller_error = match done_rx.await.unwrap() {
Ok(_) => panic!("append unexpectedly succeeded"),
Err(error) => error,
};
assert!(matches!(caller_error, SlateDBError::Fenced));
assert_eq!(db.get(b"key").await.unwrap(), None);
db.close().await.unwrap();
}
#[tokio::test]
async fn test_flush_error_notifies_caller_and_fails_handler() {
let object_store = Arc::new(InMemory::new());
let db = Db::open(
"/tmp/test_flush_error_notifies_caller_and_fails_handler",
object_store,
)
.await
.unwrap();
let wal_writer = Box::new(FailingWalWriter::new(FailingWalOperation::Flush));
let mut handler = WriteBatchEventHandler::new(db.inner.clone(), Some(wal_writer));
let (done, done_rx) = tokio::sync::oneshot::channel();
let msg = BatchWriterMessage::Flush(BatchWriterFlush {
freeze_memtable: false,
done,
});
let handler_error = handler.handle(msg).await.unwrap_err();
assert!(matches!(handler_error, SlateDBError::Fenced));
let caller_error = match done_rx.await.unwrap() {
Ok(_) => panic!("flush unexpectedly succeeded"),
Err(error) => error,
};
assert!(matches!(caller_error, SlateDBError::Fenced));
db.close().await.unwrap();
}
#[tokio::test]
async fn test_user_defined_seqnum() {
let object_store = Arc::new(InMemory::new());
let db = Db::open("/tmp/test_user_defined_seqnum", object_store)
.await
.unwrap();
let wal_writer = Box::new(FakeWalWriter::new(0));
let mut handler = WriteBatchEventHandler::new(db.inner.clone(), Some(wal_writer));
let mut batch = WriteBatch::new();
batch.put(b"key1", b"value1");
let (msg, done_rx) = test_message(
batch,
WriteOptions {
seqnum: 42,
..Default::default()
},
);
handler.handle(msg).await.unwrap();
let write_handle = done_rx.await.unwrap().unwrap();
assert_eq!(write_handle.seqnum(), 42);
let mut batch = WriteBatch::new();
batch.put(b"key2", b"value2");
let (msg, done_rx) = test_message(batch, WriteOptions::default());
handler.handle(msg).await.unwrap();
let write_handle = done_rx.await.unwrap().unwrap();
assert!(write_handle.seqnum() > 42);
}
#[tokio::test]
async fn test_user_defined_seqnum_rejects_lower_value() {
let object_store = Arc::new(InMemory::new());
let db = Db::open(
"/tmp/test_user_defined_seqnum_rejects_lower_value",
object_store,
)
.await
.unwrap();
let wal_writer = Box::new(FakeWalWriter::new(0));
let mut handler = WriteBatchEventHandler::new(db.inner.clone(), Some(wal_writer));
let mut batch = WriteBatch::new();
batch.put(b"key1", b"value1");
let (msg, done_rx) = test_message(batch, WriteOptions::default());
handler.handle(msg).await.unwrap();
let write_handle = done_rx.await.unwrap().unwrap();
let first_seq = write_handle.seqnum();
let mut batch = WriteBatch::new();
batch.put(b"key2", b"value2");
let (msg, done_rx) = test_message(
batch,
WriteOptions {
seqnum: 1,
..Default::default()
},
);
handler.handle(msg).await.unwrap();
let result = done_rx.await.unwrap();
assert!(matches!(
result,
Err(SlateDBError::InvalidSequenceNumber {
provided: 1,
current,
}) if current == first_seq
));
}
fn batch(prefixes: &[&[u8]]) -> BTreeSet<Bytes> {
prefixes.iter().map(|p| Bytes::copy_from_slice(p)).collect()
}
fn assert_invalid_segment_prefix(err: SlateDBError, prefix: &[u8], conflict: &[u8]) {
match err {
SlateDBError::InvalidSegmentPrefix {
prefix: p,
conflict: c,
} => {
assert_eq!(p.as_ref(), prefix);
assert_eq!(c.as_ref(), conflict);
}
other => panic!("expected InvalidSegmentPrefix, got {other:?}"),
}
}
#[test]
fn check_batch_antichain_accepts_empty_set() {
check_batch_antichain(&BTreeSet::new()).unwrap();
}
#[test]
fn check_batch_antichain_accepts_singleton() {
check_batch_antichain(&batch(&[b"abc"])).unwrap();
}
#[test]
fn check_batch_antichain_accepts_disjoint_prefixes() {
check_batch_antichain(&batch(&[b"aaa", b"bbb", b"ccc"])).unwrap();
}
#[test]
fn check_batch_antichain_rejects_ancestor_descendant_pair() {
let err = check_batch_antichain(&batch(&[b"abc", b"abcd"])).unwrap_err();
assert_invalid_segment_prefix(err, b"abcd", b"abc");
}
#[test]
fn check_batch_antichain_rejects_when_nesting_is_not_at_input_adjacency() {
let err = check_batch_antichain(&batch(&[b"abc", b"z", b"abcd"])).unwrap_err();
assert_invalid_segment_prefix(err, b"abcd", b"abc");
}
async fn open_db_with_extractor(
path: &str,
extractor: Arc<dyn crate::prefix_extractor::PrefixExtractor>,
) -> Db {
let object_store: Arc<dyn object_store::ObjectStore> = Arc::new(InMemory::new());
Db::builder(path, object_store)
.with_segment_extractor(extractor)
.build()
.await
.unwrap()
}
#[tokio::test]
async fn write_with_extractor_accepts_disjoint_prefixes_in_one_batch() {
let db = open_db_with_extractor(
"/tmp/test_write_disjoint_prefixes",
Arc::new(crate::test_utils::FixedThreeBytePrefixExtractor),
)
.await;
let mut batch = WriteBatch::new();
batch.put(b"aaa-1", b"v1");
batch.put(b"bbb-1", b"v2");
batch.put(b"ccc-1", b"v3");
db.write(batch).await.unwrap();
for (k, v) in [
(&b"aaa-1"[..], &b"v1"[..]),
(&b"bbb-1"[..], &b"v2"[..]),
(&b"ccc-1"[..], &b"v3"[..]),
] {
let got = db.get(k).await.unwrap().unwrap();
assert_eq!(got.as_ref(), v);
}
db.close().await.unwrap();
}
#[tokio::test]
async fn write_rejects_intra_batch_nesting_prefixes() {
let db = open_db_with_extractor(
"/tmp/test_write_intra_batch_nesting",
Arc::new(crate::test_utils::NonAntichainTestPrefixExtractor),
)
.await;
let mut batch = WriteBatch::new();
batch.put(b"abc-x", b"v1"); batch.put(b"ab-y", b"v2"); let err = db.write(batch).await.unwrap_err();
assert!(matches!(err.kind(), crate::error::ErrorKind::Invalid));
assert!(db.get(b"abc-x").await.unwrap().is_none());
assert!(db.get(b"ab-y").await.unwrap().is_none());
db.close().await.unwrap();
}
#[tokio::test]
async fn write_rejects_when_new_prefix_nests_existing_segment() {
let db = open_db_with_extractor(
"/tmp/test_write_nests_existing_segment",
Arc::new(crate::test_utils::NonAntichainTestPrefixExtractor),
)
.await;
let mut batch = WriteBatch::new();
batch.put(b"abc-1", b"v1"); db.write(batch).await.unwrap();
db.flush_with_options(crate::config::FlushOptions {
flush_type: crate::config::FlushType::MemTable,
})
.await
.unwrap();
{
let guard = db.inner.state.read();
let cow = guard.state();
let prefixes: Vec<&[u8]> = cow
.core()
.segments
.iter()
.map(|s| s.prefix.as_ref())
.collect();
assert!(
prefixes.iter().any(|p| *p == &b"abc"[..]),
"expected segment 'abc' to be persisted; got {:?}",
prefixes
);
}
let mut batch = WriteBatch::new();
batch.put(b"ab-1", b"v2");
let err = db.write(batch).await.unwrap_err();
assert!(matches!(err.kind(), crate::error::ErrorKind::Invalid));
assert!(db.get(b"ab-1").await.unwrap().is_none());
db.close().await.unwrap();
}
#[tokio::test]
async fn write_rejects_when_new_prefix_nests_active_memtable_prefix() {
let db = open_db_with_extractor(
"/tmp/test_write_nests_memtable_prefix",
Arc::new(crate::test_utils::NonAntichainTestPrefixExtractor),
)
.await;
let mut first = WriteBatch::new();
first.put(b"abc-1", b"v1");
db.write(first).await.unwrap();
{
let guard = db.inner.state.read();
assert!(
guard.state().core().segments.is_empty(),
"manifest must not yet contain any segment for this test to be meaningful"
);
}
let mut second = WriteBatch::new();
second.put(b"ab-2", b"v2");
let err = db.write(second).await.unwrap_err();
assert!(matches!(err.kind(), crate::error::ErrorKind::Invalid));
assert!(db.get(b"ab-2").await.unwrap().is_none());
assert_eq!(db.get(b"abc-1").await.unwrap().unwrap().as_ref(), b"v1");
db.close().await.unwrap();
}
#[tokio::test]
async fn write_rejects_empty_extractor_prefix() {
#[derive(Debug)]
struct AlwaysEmptyExtractor;
impl crate::prefix_extractor::PrefixExtractor for AlwaysEmptyExtractor {
fn name(&self) -> &str {
"always-empty"
}
fn prefix_len(&self, _target: &crate::prefix_extractor::PrefixTarget) -> Option<usize> {
Some(0)
}
}
let db = open_db_with_extractor(
"/tmp/test_write_empty_prefix",
Arc::new(AlwaysEmptyExtractor),
)
.await;
let mut batch = WriteBatch::new();
batch.put(b"any-key", b"v1");
let err = db.write(batch).await.unwrap_err();
assert!(matches!(err.kind(), crate::error::ErrorKind::Invalid));
assert!(
err.to_string().contains("empty prefix"),
"expected empty-prefix error, got: {err}"
);
assert!(db.get(b"any-key").await.unwrap().is_none());
db.close().await.unwrap();
}
#[tokio::test]
async fn write_without_extractor_accepts_arbitrary_keys() {
let object_store = Arc::new(InMemory::new());
let db = Db::open("/tmp/test_write_no_extractor", object_store)
.await
.unwrap();
let mut batch = WriteBatch::new();
batch.put(b"a", b"v1");
batch.put(b"abc", b"v2");
batch.put(b"abcdef", b"v3");
db.write(batch).await.unwrap();
for (k, v) in [
(&b"a"[..], &b"v1"[..]),
(&b"abc"[..], &b"v2"[..]),
(&b"abcdef"[..], &b"v3"[..]),
] {
assert_eq!(db.get(k).await.unwrap().unwrap().as_ref(), v);
}
db.close().await.unwrap();
}
}