use std::path::Path;
use arrow_array::{Array, Float32Array, Int32Array, RecordBatch};
use futures::TryStreamExt;
use paimon::catalog::Identifier;
use paimon::io::{FileIO, FileIOBuilder};
use paimon::table::{SchemaManager, Table};
const VECTOR_COLUMN: &str = "embedding";
const FIXTURE: &str = "testdata/pkvector/pk_vector_ivf_flat";
const VECTORS: &[[f32; 2]] = &[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0], [4.0, 0.0]];
fn l2_score(distance: f32) -> f32 {
1.0 / (1.0 + distance)
}
fn analytic_topk(query: &[f32], k: usize) -> Vec<(u64, f32)> {
let mut scored: Vec<(u64, f32)> = VECTORS
.iter()
.enumerate()
.map(|(pos, v)| {
let dist: f32 = v
.iter()
.zip(query.iter())
.map(|(a, b)| (a - b) * (a - b))
.sum();
(pos as u64, dist)
})
.collect();
scored.sort_by(|a, b| a.1.total_cmp(&b.1));
scored.truncate(k);
scored
}
async fn open_java_fixture() -> (tempfile::TempDir, Table) {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let src = Path::new(manifest_dir).join(FIXTURE);
let tmp = tempfile::tempdir().expect("create temp dir");
let dst = tmp.path().join("pk_vector_ivf_flat");
copy_dir(&src, &dst);
let location = format!("file://{}", dst.display());
let file_io: FileIO = FileIOBuilder::new("file").build().expect("build fs FileIO");
let schema = SchemaManager::new(file_io.clone(), location.clone())
.latest()
.await
.expect("failed to list schemas")
.expect("fixture table has no schema");
let table = Table::new(
file_io,
Identifier::new("default", "pk_vector_ivf_flat"),
location,
(*schema).clone(),
None,
);
(tmp, table)
}
fn copy_dir(src: &Path, dst: &Path) {
std::fs::create_dir_all(dst).unwrap();
for entry in std::fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let from = entry.path();
let to = dst.join(entry.file_name());
if from.is_dir() {
copy_dir(&from, &to);
} else {
std::fs::copy(&from, &to).unwrap();
}
}
}
fn batch_i32(batches: &[RecordBatch], col: &str) -> Vec<i32> {
batches
.iter()
.flat_map(|b| {
let idx = b.schema().index_of(col).unwrap();
b.column(idx)
.as_any()
.downcast_ref::<Int32Array>()
.unwrap()
.values()
.to_vec()
})
.collect()
}
fn batch_f32(batches: &[RecordBatch], col: &str) -> Vec<f32> {
batches
.iter()
.flat_map(|b| {
let idx = b.schema().index_of(col).unwrap();
b.column(idx)
.as_any()
.downcast_ref::<Float32Array>()
.unwrap()
.values()
.to_vec()
})
.collect()
}
#[cfg(not(target_os = "windows"))]
#[tokio::test]
async fn reads_back_java_written_pk_vector_table() {
let (_tmp, table) = open_java_fixture().await;
let query = vec![0.0f32, 0.0];
let k = 3;
let expected = analytic_topk(&query, k);
let expected_ids: Vec<i32> = expected.iter().map(|(id, _)| *id as i32).collect();
let expected_scores: Vec<f32> = expected.iter().map(|(_, d)| l2_score(*d)).collect();
assert_eq!(
expected_ids,
vec![0, 1, 2],
"fixture top-3 ids must be [0, 1, 2]"
);
let scored = table
.new_vector_search_builder()
.with_vector_column(VECTOR_COLUMN)
.with_query_vector(query.clone())
.with_limit(k)
.execute_scored()
.await;
assert!(
scored.is_err(),
"execute_scored() must fail on a primary-key vector table: global row ids \
are unavailable when the data files carry no first_row_id"
);
let mut builder = table.new_vector_search_builder();
builder
.with_vector_column(VECTOR_COLUMN)
.with_query_vector(query)
.with_limit(k)
.with_projection(&["id"]);
let batches = builder
.execute_read()
.await
.expect("primary-key vector read over the Java fixture failed")
.try_collect::<Vec<_>>()
.await
.expect("collecting read batches failed");
let ids = batch_i32(&batches, "id");
assert_eq!(
ids, expected_ids,
"materialized `id` column must be best-first and match the analytic top-k"
);
let scores = batch_f32(&batches, "__paimon_search_score");
assert_eq!(scores.len(), k);
for (got, want) in scores.iter().zip(&expected_scores) {
assert!(
(got - want).abs() < 1e-4,
"materialized score diverges: got {got}, want {want}"
);
}
}