#![deny(clippy::unwrap_used)]
use std::{
ops::Range,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
};
use arrow_array::{ArrayRef, FixedSizeListArray, Float32Array, LargeStringArray, RecordBatch};
use arrow_schema::{DataType, Field, Schema};
use async_trait::async_trait;
use bytes::Bytes;
use infino::{
storage::{LocalFsStorageProvider, ObjectMeta, StorageError, StorageProvider},
superfile::builder::FtsConfig,
supertable::{Supertable, SupertableOptions, manifest::commit::POINTER_PATH},
test_helpers::{default_tokenizer, default_vector_config},
};
use tempfile::TempDir;
#[derive(Debug)]
struct PointerHiddenOnce {
inner: Arc<dyn StorageProvider>,
hidden_probes_left: AtomicUsize,
}
impl PointerHiddenOnce {
fn new(inner: Arc<dyn StorageProvider>) -> Arc<Self> {
Arc::new(Self {
inner,
hidden_probes_left: AtomicUsize::new(1),
})
}
}
#[async_trait]
impl StorageProvider for PointerHiddenOnce {
async fn head(&self, uri: &str) -> Result<ObjectMeta, StorageError> {
self.inner.head(uri).await
}
async fn get(&self, uri: &str) -> Result<(Bytes, ObjectMeta), StorageError> {
if uri == POINTER_PATH
&& self
.hidden_probes_left
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |n| n.checked_sub(1))
.is_ok()
{
return Err(StorageError::NotFound { uri: uri.into() });
}
self.inner.get(uri).await
}
async fn get_range(&self, uri: &str, range: Range<u64>) -> Result<Bytes, StorageError> {
self.inner.get_range(uri, range).await
}
async fn put_atomic(&self, uri: &str, bytes: Bytes) -> Result<Option<String>, StorageError> {
self.inner.put_atomic(uri, bytes).await
}
async fn put_if_match(
&self,
uri: &str,
bytes: Bytes,
expected_etag: Option<&str>,
) -> Result<Option<String>, StorageError> {
self.inner.put_if_match(uri, bytes, expected_etag).await
}
async fn put_multipart(
&self,
uri: &str,
) -> Result<Box<dyn object_store::MultipartUpload>, StorageError> {
self.inner.put_multipart(uri).await
}
async fn delete(&self, uri: &str) -> Result<(), StorageError> {
self.inner.delete(uri).await
}
}
const DIM: usize = 16;
const VECTOR_ROT_SEED: u64 = 31;
const TITLES: &[&str] = &["alpha document", "bravo document", "charlie document"];
fn fixed_list_f32(dim: usize) -> DataType {
DataType::FixedSizeList(
Arc::new(Field::new("item", DataType::Float32, true)),
dim as i32,
)
}
fn vector_options() -> SupertableOptions {
let schema = Arc::new(Schema::new(vec![
Field::new("title", DataType::LargeUtf8, false),
Field::new("emb", fixed_list_f32(DIM), false),
]));
SupertableOptions::new(
schema,
vec![FtsConfig {
column: "title".into(),
positions: false,
}],
vec![default_vector_config("emb", VECTOR_ROT_SEED)],
Some(default_tokenizer()),
)
.expect("valid options")
}
fn one_hot_batch(schema: Arc<Schema>) -> RecordBatch {
let n = TITLES.len();
let mut flat = Vec::<f32>::with_capacity(n * DIM);
for i in 0..n {
for d in 0..DIM {
flat.push(if d == i % DIM { 1.0 } else { 0.0 });
}
}
let fsl = FixedSizeListArray::try_new(
Arc::new(Field::new("item", DataType::Float32, true)),
DIM as i32,
Arc::new(Float32Array::from(flat)) as ArrayRef,
None,
)
.expect("FSL");
RecordBatch::try_new(
schema,
vec![
Arc::new(LargeStringArray::from(TITLES.to_vec())),
Arc::new(fsl),
],
)
.expect("batch")
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn losing_creator_adopts_winner_manifest_and_hidden_index() {
let dir = TempDir::new().expect("tempdir");
let storage: Arc<dyn StorageProvider> =
Arc::new(LocalFsStorageProvider::new(dir.path()).expect("provider"));
let winner = Supertable::create(vector_options().with_storage(Arc::clone(&storage)))
.expect("winner create");
let schema = winner.options().schema.clone();
let mut w = winner.writer().expect("writer");
w.append(&one_hot_batch(schema)).expect("append");
w.commit().expect("commit");
assert_eq!(winner.manifest_id(), 1);
let racing: Arc<dyn StorageProvider> = PointerHiddenOnce::new(Arc::clone(&storage));
let loser = Supertable::create(vector_options().with_storage(racing))
.expect("create-or-open must adopt, not fail");
assert_eq!(
loser.manifest_id(),
1,
"the adopted view is the winner's committed manifest"
);
assert_eq!(
loser.reader().expect("reader").n_docs_total(),
TITLES.len() as u64
);
let schema = loser.options().schema.clone();
let mut w = loser.writer().expect("loser writer");
w.append(&one_hot_batch(schema))
.expect("append via adopted");
w.commit().expect("commit via adopted");
drop(w);
loser
.drain_vectors_to_cells_sync()
.expect("drain through the adopted handle");
let fresh =
Supertable::open(vector_options().with_storage(Arc::clone(&storage))).expect("fresh open");
assert_eq!(
fresh.reader().expect("reader").n_docs_total(),
2 * TITLES.len() as u64,
"both writers' commits are visible"
);
let fresh_hidden = fresh.vector_index_table().expect("hidden handle");
assert_eq!(
fresh_hidden.reader().expect("hidden reader").n_docs_total(),
2 * TITLES.len() as u64,
"vectors routed through the adopted handle must land under the \
manifest-stamped hidden prefix; a shortfall means the loser \
drained into its own orphaned prefix"
);
}