1use std::path::{Path, PathBuf};
8
9use arrow::array::{Array as ArrowArray, Float64Array, UInt32Array};
10use arrow::datatypes::{DataType, Float64Type, Int64Type, UInt32Type};
11use arrow::record_batch::RecordBatch;
12use log::{debug, info};
13use smartcore::linalg::basic::arrays::Array;
14use smartcore::linalg::basic::matrix::DenseMatrix;
15use sprs::CsMat;
16
17use crate::metadata::FileInfo;
18use crate::metadata::GeneMetadata;
19use crate::traits::backend::StorageBackend;
20use crate::traits::lance::LanceStorage;
21use crate::traits::metadata::Metadata;
22use crate::{StorageError, StorageResult};
23
24fn checked_u32_values(values: &[usize], what: &str) -> StorageResult<Vec<u32>> {
29 values
30 .iter()
31 .map(|&v| {
32 u32::try_from(v).map_err(|_| {
33 StorageError::Overflow(format!(
34 "{} value {} exceeds u32::MAX and would be silently truncated",
35 what, v
36 ))
37 })
38 })
39 .collect()
40}
41
42#[derive(Debug, Clone)]
95pub struct LanceStorageGraph {
96 pub(crate) base: String,
97 pub(crate) name: String,
98}
99
100impl LanceStorageGraph {
101 pub fn new(base: String, name: String) -> Self {
110 info!("Creating LanceStorage at base={}, name={}", base, name);
111 Self { base, name }
112 }
113
114 pub async fn spawn(base_path: String) -> Result<(Self, GeneMetadata), StorageError> {
116 let (exists, md_path) = Self::exists(&base_path);
118
119 if !exists || md_path.is_none() {
121 return Err(StorageError::Invalid(format!(
122 "Metadata does not exist in base path: {}",
123 base_path
124 )));
125 }
126
127 let metadata = GeneMetadata::read(md_path.unwrap()).await?;
129
130 let storage = Self::new(base_path.clone(), metadata.name_id.clone());
132 Ok((storage, metadata))
133 }
134
135 pub fn scoped_generation(&self, generation: u64) -> Self {
141 let logical = crate::generations::logical_name(&self.name);
142 Self::new(
143 self.base.clone(),
144 crate::generations::generation_name(logical, generation),
145 )
146 }
147}
148
149impl LanceStorage for LanceStorageGraph {}
150
151impl StorageBackend for LanceStorageGraph {
152 fn get_base(&self) -> String {
153 self.base.clone()
154 }
155
156 fn get_name(&self) -> String {
157 self.name.clone()
158 }
159
160 fn base_path(&self) -> PathBuf {
161 PathBuf::from(&self.base)
162 }
163
164 fn metadata_path(&self) -> PathBuf {
165 self.base_path()
166 .join(format!("{}_metadata.json", self.name))
167 }
168
169 fn basepath_to_uri(&self) -> StorageResult<String> {
171 Self::path_to_uri(PathBuf::from(self.base.clone()).as_path())
172 }
173
174 async fn save_dense(
184 &self,
185 key: &str,
186 matrix: &DenseMatrix<f64>,
187 md_path: &Path,
188 ) -> StorageResult<()> {
189 self.validate_initialized(md_path)?;
190 let path = self.file_path(key);
191 let (n_rows, n_cols) = matrix.shape();
192
193 info!(
194 "Saving dense {} matrix: {} x {} at {:?}",
195 key, n_rows, n_cols, path
196 );
197
198 let batch = self.to_dense_record_batch(matrix)?;
200
201 if batch.num_rows() != n_rows {
203 return Err(StorageError::Invalid(format!(
204 "RecordBatch has {} rows but matrix has {} rows",
205 batch.num_rows(),
206 n_rows
207 )));
208 }
209
210 {
211 let uri = Self::path_to_uri(&path)?;
213 self.write_lance_batch_async(uri, batch).await?;
214 let mut md = self.load_metadata().await?;
215 md = md.add_file(
216 key,
217 FileInfo::new(
218 format!("{}_{}.lance", self.get_name(), key),
219 "dense",
220 matrix.shape(),
221 None,
222 None,
223 )?,
224 );
225 self.save_metadata(&md).await?;
226 info!("Dense {} matrix saved successfully", key);
227 }
228 Ok(())
229 }
230
231 async fn load_dense(&self, key: &str) -> StorageResult<DenseMatrix<f64>> {
241 let path = self.file_path(key);
242 info!("Loading dense {} matrix from {:?}", key, path);
243
244 let uri = Self::path_to_uri(&path)?;
246 let batch = self.read_lance_all_batches_async(uri).await?;
247
248 let matrix = self.from_dense_record_batch(&batch)?;
250
251 let (n_rows, n_cols) = matrix.shape();
252 info!("Loaded dense {} matrix: {} x {}", key, n_rows, n_cols);
253
254 Ok(matrix)
255 }
256
257 async fn load_dense_from_file(&self, path: &Path) -> StorageResult<DenseMatrix<f64>> {
261 info!("Loading dense matrix from file (async): {:?}", path);
262
263 if !path.exists() {
264 return Err(StorageError::Invalid(format!(
265 "Dense file does not exist: {:?}",
266 path
267 )));
268 }
269
270 let extension = path
271 .extension()
272 .and_then(|e| e.to_str())
273 .ok_or_else(|| StorageError::Invalid(format!("Invalid file path: {:?}", path)))?;
274
275 match extension {
276 "lance" => {
277 let parent = path
280 .parent()
281 .ok_or_else(|| {
282 StorageError::Invalid(format!("Path has no parent: {:?}", path))
283 })?
284 .to_str()
285 .ok_or_else(|| {
286 StorageError::Invalid(format!("Non-UTF8 parent path for {:?}", path))
287 })?
288 .to_string();
289
290 let tmp_storage = Self::new(parent, String::from("tmp_storage"));
291
292 let uri = Self::path_to_uri(path)?;
294 let batch = tmp_storage.read_lance_all_batches_async(uri).await?;
295 let matrix = tmp_storage.from_dense_record_batch(&batch)?;
296 info!(
297 "Loaded dense matrix from Lance: {} x {}",
298 matrix.shape().0,
299 matrix.shape().1
300 );
301 Ok(matrix)
302 }
303 "parquet" => {
304 use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
305
306 let owned_path = path.to_path_buf();
310 let combined =
311 tokio::task::spawn_blocking(move || -> StorageResult<RecordBatch> {
312 let file = std::fs::File::open(&owned_path).map_err(|e| {
313 StorageError::Io(format!("Failed to open parquet file: {}", e))
314 })?;
315
316 let builder =
317 ParquetRecordBatchReaderBuilder::try_new(file).map_err(|e| {
318 StorageError::Parquet(format!(
319 "Failed to create parquet reader: {}",
320 e
321 ))
322 })?;
323 let reader = builder.build().map_err(|e| {
324 StorageError::Parquet(format!("Failed to build parquet reader: {}", e))
325 })?;
326
327 let batches: Vec<RecordBatch> =
328 reader.collect::<Result<Vec<_>, _>>().map_err(|e| {
329 StorageError::Parquet(format!(
330 "Failed to read parquet batch: {}",
331 e
332 ))
333 })?;
334
335 if batches.is_empty() {
336 return Err(StorageError::Invalid(format!(
337 "Empty parquet dataset at {:?}",
338 owned_path
339 )));
340 }
341
342 let schema = batches[0].schema();
343 arrow::compute::concat_batches(&schema, &batches).map_err(|e| {
344 StorageError::Parquet(format!(
345 "Failed to concatenate parquet batches: {}",
346 e
347 ))
348 })
349 })
350 .await
351 .map_err(|e| {
352 StorageError::Io(format!("Parquet reader task failed: {}", e))
353 })??;
354
355 let schema = combined.schema();
357 let fields = schema.fields();
358 let is_vector = fields.len() == 1
359 && matches!(
360 fields[0].data_type(),
361 DataType::FixedSizeList(inner, _)
362 if matches!(inner.data_type(), DataType::Float64)
363 );
364
365 let is_wide_col = !is_vector
366 && !fields.is_empty()
367 && fields
368 .iter()
369 .all(|f| matches!(f.data_type(), DataType::Float64))
370 && fields.iter().any(|f| f.name().starts_with("col_"));
371
372 let matrix = if is_vector {
374 let parent = path
377 .parent()
378 .ok_or_else(|| {
379 StorageError::Invalid(format!("Path has no parent: {:?}", path))
380 })?
381 .to_str()
382 .ok_or_else(|| {
383 StorageError::Invalid(format!("Non-UTF8 parent path for {:?}", path))
384 })?
385 .to_string();
386
387 let tmp_storage = Self::new(parent, String::from("tmp_storage"));
388 tmp_storage.from_dense_record_batch(&combined)?
389 } else if is_wide_col {
390 let n_rows = combined.num_rows();
392 let n_cols = combined.num_columns();
393 if n_rows == 0 || n_cols == 0 {
394 return Err(StorageError::Invalid(format!(
395 "Cannot load empty wide-column parquet at {:?}",
396 path
397 )));
398 }
399
400 let mut data = Vec::with_capacity(n_rows * n_cols);
401 for col_idx in 0..n_cols {
402 let col = combined.column(col_idx);
403 let arr = col.as_any().downcast_ref::<Float64Array>().ok_or_else(|| {
404 StorageError::Invalid(format!(
405 "Wide-column parquet expects Float64, got {:?} in column {}",
406 col.data_type(),
407 col_idx
408 ))
409 })?;
410 for row_idx in 0..n_rows {
412 data.push(arr.value(row_idx));
413 }
414 }
415
416 DenseMatrix::new(n_rows, n_cols, data, true)
417 .map_err(|e| StorageError::Invalid(e.to_string()))?
418 } else {
419 return Err(StorageError::Invalid(format!(
420 "Unsupported Parquet schema at {:?}: expected FixedSizeList<Float64> \
421 or wide Float64 columns named col_*",
422 path
423 )));
424 };
425
426 info!(
427 "Loaded dense matrix from Parquet: {} x {}",
428 matrix.shape().0,
429 matrix.shape().1
430 );
431
432 Ok(matrix)
433 }
434 _ => Err(StorageError::Invalid(format!(
435 "Unsupported file format: {}. Only .lance and .parquet are supported",
436 extension
437 ))),
438 }
439 }
440
441 fn file_path(&self, key: &str) -> PathBuf {
442 self.base_path()
443 .join(format!("{}_{}.lance", self.name, key))
444 }
445
446 async fn save_sparse(
451 &self,
452 key: &str,
453 matrix: &CsMat<f64>,
454 md_path: &Path,
455 ) -> StorageResult<()> {
456 self.validate_initialized(md_path)?;
457 let path = self.file_path(key);
458 info!(
459 "Saving sparse {} matrix: {} x {}, nnz={} at {:?}",
460 key,
461 matrix.rows(),
462 matrix.cols(),
463 matrix.nnz(),
464 path
465 );
466
467 let filetype = FileInfo::which_filetype(key)?;
468 {
469 let mut metadata = self.load_metadata().await?;
470 metadata = metadata.add_file(
471 key,
472 FileInfo::new(
473 format!("{}_{}.lance", self.get_name(), key),
474 filetype.as_str(),
475 (matrix.rows(), matrix.cols()),
476 Some(matrix.nnz()),
477 None,
478 )?,
479 );
480 self.save_metadata(&metadata).await?;
481
482 let batch = self.to_sparse_record_batch(matrix)?;
483 let uri = Self::path_to_uri(&path)?;
484 self.write_lance_batch_async(uri, batch).await?;
485 }
486 info!("Sparse matrix {} saved successfully", filetype);
487 Ok(())
488 }
489
490 async fn load_sparse(&self, key: &str) -> StorageResult<CsMat<f64>> {
491 info!("Loading sparse {} matrix", key);
492
493 let metadata = self.load_metadata().await?;
494 let filetype = FileInfo::which_filetype(key)?;
495 let file_info = metadata
496 .files
497 .get(key)
498 .ok_or_else(|| StorageError::Invalid(format!("{key} not found in metadata")))?;
499
500 let expected_rows = file_info.rows;
501 let expected_cols = file_info.cols;
502 debug!(
503 "Expected dimensions from storage metadata: {} x {}",
504 expected_rows, expected_cols
505 );
506
507 let path = self.file_path(key);
508 let uri = Self::path_to_uri(&path)?;
509 let batch = self.read_lance_all_batches_async(uri).await?;
510 let matrix = self.from_sparse_record_batch(batch, expected_rows, expected_cols)?;
511 info!(
512 "Sparse {} matrix loaded: {} x {}, nnz={}",
513 filetype,
514 matrix.rows(),
515 matrix.cols(),
516 matrix.nnz()
517 );
518 Ok(matrix)
519 }
520
521 async fn save_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()> {
522 info!("Saving {} lambda values", lambdas.len());
523 self.save_primitive_column::<Float64Type>("lambdas", "lambda", lambdas.to_vec(), md_path)
524 .await
525 }
526
527 async fn load_lambdas(&self) -> StorageResult<Vec<f64>> {
528 let path = self.file_path("lambdas");
529 info!("Loading lambda values from {:?}", path);
530
531 let uri = Self::path_to_uri(&path)?;
532 let batch = self.read_lance_all_batches_async(uri).await?;
533 let arr = batch
534 .column(0)
535 .as_any()
536 .downcast_ref::<Float64Array>()
537 .ok_or_else(|| StorageError::Invalid("lambda column type mismatch".into()))?;
538
539 let lambdas: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
540 info!("Loaded {} lambda values", lambdas.len());
541 Ok(lambdas)
542 }
543
544 async fn save_vector(&self, key: &str, vector: &[f64], md_path: &Path) -> StorageResult<()> {
545 info!("Saving {} values for vector {}", vector.len(), key);
546 self.save_primitive_column::<Float64Type>(key, "element", vector.to_vec(), md_path)
547 .await
548 }
549
550 async fn save_index(&self, key: &str, vector: &[usize], md_path: &Path) -> StorageResult<()> {
551 info!("Saving {} values for index {}", vector.len(), key);
552 let values = checked_u32_values(vector, "index")?;
555 self.save_primitive_column::<UInt32Type>(key, "id", values, md_path)
556 .await
557 }
558
559 async fn load_vector(&self, filename: &str) -> StorageResult<Vec<f64>> {
560 let path = self.file_path(filename);
561 info!("Loading vector {} from {:?}", filename, path);
562
563 let uri = Self::path_to_uri(&path)?;
564 let batch = self.read_lance_all_batches_async(uri).await?;
565 let arr = batch
566 .column(0)
567 .as_any()
568 .downcast_ref::<Float64Array>()
569 .ok_or_else(|| StorageError::Invalid("column type mismatch".into()))?;
570
571 let vector: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
572 info!("Loaded {} vector values for {}", vector.len(), filename);
573 Ok(vector)
574 }
575
576 async fn load_index(&self, filename: &str) -> StorageResult<Vec<usize>> {
577 let path = self.file_path(filename);
578 info!("Loading vector {} from {:?}", filename, path);
579
580 let uri = Self::path_to_uri(&path)?;
581 let batch = self.read_lance_all_batches_async(uri).await?;
582 let arr = batch
583 .column(0)
584 .as_any()
585 .downcast_ref::<UInt32Array>()
586 .ok_or_else(|| StorageError::Invalid("column type mismatch".into()))?;
587
588 let vector: Vec<usize> = (0..arr.len()).map(|i| arr.value(i) as usize).collect();
589 info!("Loaded {} vector values for {}", vector.len(), filename);
590 Ok(vector)
591 }
592
593 async fn save_dense_to_file(data: &DenseMatrix<f64>, path: &Path) -> StorageResult<()> {
597 info!("Saving dense matrix to file (async): {:?}", path);
598
599 let parent = path
602 .parent()
603 .filter(|p| !p.as_os_str().is_empty())
604 .ok_or_else(|| {
605 StorageError::Invalid(format!("path has no parent directory: {:?}", path))
606 })?;
607 tokio::fs::create_dir_all(parent)
608 .await
609 .map_err(|e| StorageError::Io(format!("create dir {:?}: {}", parent, e)))?;
610 let parent_str = parent
611 .to_str()
612 .ok_or_else(|| StorageError::Invalid(format!("non-UTF8 parent path for {:?}", path)))?
613 .to_string();
614
615 let tmp_storage = Self::new(parent_str, String::from("tmp_storage"));
617
618 let extension = path
619 .extension()
620 .and_then(|e| e.to_str())
621 .ok_or_else(|| StorageError::Invalid(format!("Invalid file path: {:?}", path)))?;
622
623 let (n_rows, n_cols) = data.shape();
624 info!("Saving matrix: {} rows x {} cols", n_rows, n_cols);
625
626 match extension {
627 "lance" => {
628 let batch = tmp_storage.to_dense_record_batch(data)?;
629 debug!(
630 "Created RecordBatch with {} rows for Lance",
631 batch.num_rows()
632 );
633
634 if batch.num_rows() != n_rows {
636 return Err(StorageError::Invalid(format!(
637 "RecordBatch has {} rows but matrix has {} rows",
638 batch.num_rows(),
639 n_rows
640 )));
641 }
642
643 let uri = Self::path_to_uri(path)?;
644 tmp_storage.write_lance_batch_async(uri, batch).await?;
645 info!("Saved dense matrix to Lance: {} x {}", n_rows, n_cols);
646 Ok(())
647 }
648 "parquet" => {
649 use parquet::arrow::ArrowWriter;
650 use parquet::file::properties::WriterProperties;
651 use std::fs::File;
652
653 let batch = tmp_storage.to_dense_record_batch(data)?;
654 debug!(
655 "Created RecordBatch with {} rows for Parquet",
656 batch.num_rows()
657 );
658
659 if batch.num_rows() != n_rows {
660 return Err(StorageError::Invalid(format!(
661 "RecordBatch has {} rows but matrix has {} rows",
662 batch.num_rows(),
663 n_rows
664 )));
665 }
666
667 let owned_path = path.to_path_buf();
671 tokio::task::spawn_blocking(move || -> StorageResult<()> {
672 let file = File::create(&owned_path).map_err(|e| {
673 StorageError::Io(format!("Failed to create parquet file: {}", e))
674 })?;
675
676 let props = WriterProperties::builder()
677 .set_compression(parquet::basic::Compression::SNAPPY)
678 .build();
679
680 let mut writer = ArrowWriter::try_new(file, batch.schema(), Some(props))
681 .map_err(|e| {
682 StorageError::Parquet(format!("Failed to create parquet writer: {}", e))
683 })?;
684
685 writer.write(&batch).map_err(|e| {
686 StorageError::Parquet(format!("Failed to write batch: {}", e))
687 })?;
688
689 writer.close().map_err(|e| {
690 StorageError::Parquet(format!("Failed to close writer: {}", e))
691 })?;
692
693 Ok(())
694 })
695 .await
696 .map_err(|e| StorageError::Io(format!("parquet writer task failed: {}", e)))??;
697
698 info!("Saved dense matrix to Parquet: {} x {}", n_rows, n_cols);
699 Ok(())
700 }
701 _ => Err(StorageError::Invalid(format!(
702 "Unsupported file format: {}. Only .lance and .parquet are supported",
703 extension
704 ))),
705 }
706 }
707
708 async fn save_centroid_map(&self, map: &[usize], md_path: &Path) -> StorageResult<()> {
710 info!("Saving {} centroid map entries", map.len());
711 let values = checked_u32_values(map, "centroid map")?;
714 self.save_primitive_column::<UInt32Type>("centroid_map", "centroid_id", values, md_path)
715 .await
716 }
717
718 async fn load_centroid_map(&self) -> StorageResult<Vec<usize>> {
720 let path = self.file_path("centroid_map");
721 info!("Loading centroid map from {:?}", path);
722
723 let uri = Self::path_to_uri(&path)?;
724 let batch = self.read_lance_all_batches_async(uri).await?;
725 let arr = batch
726 .column(0)
727 .as_any()
728 .downcast_ref::<UInt32Array>()
729 .ok_or_else(|| StorageError::Invalid("centroid_id column type mismatch".into()))?;
730
731 let map: Vec<usize> = (0..arr.len()).map(|i| arr.value(i) as usize).collect();
732 info!("Loaded {} centroid map entries", map.len());
733 Ok(map)
734 }
735
736 async fn save_subcentroid_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()> {
738 info!("Saving {} subcentroid lambda values", lambdas.len());
739 self.save_primitive_column::<Float64Type>(
740 "subcentroid_lambdas",
741 "subcentroid_lambda",
742 lambdas.to_vec(),
743 md_path,
744 )
745 .await
746 }
747
748 async fn load_subcentroid_lambdas(&self) -> StorageResult<Vec<f64>> {
750 let path = self.file_path("subcentroid_lambdas");
751 info!("Loading subcentroid lambda values from {:?}", path);
752
753 let uri = Self::path_to_uri(&path)?;
754 let batch = self.read_lance_all_batches_async(uri).await?;
755 let arr = batch
756 .column(0)
757 .as_any()
758 .downcast_ref::<Float64Array>()
759 .ok_or_else(|| {
760 StorageError::Invalid("subcentroid_lambda column type mismatch".into())
761 })?;
762
763 let lambdas: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
764 info!("Loaded {} subcentroid lambda values", lambdas.len());
765 Ok(lambdas)
766 }
767
768 async fn save_subcentroids(
770 &self,
771 subcentroids: &DenseMatrix<f64>,
772 md_path: &Path,
773 ) -> StorageResult<()> {
774 self.validate_initialized(md_path)?;
775 let key = "sub_centroids";
776 let path = self.file_path(key);
777 let (n_rows, n_cols) = subcentroids.shape();
778 info!(
779 "Saving subcentroids matrix {} x {} at {:?}",
780 n_rows, n_cols, path
781 );
782
783 let batch = self.to_dense_record_batch(subcentroids)?;
784 {
785 let mut metadata = self.load_metadata().await?;
786 metadata = metadata.add_file(
787 key,
788 FileInfo::new(
789 format!("{}_{}.lance", self.get_name(), key),
790 "vector",
791 subcentroids.shape(),
792 None,
793 None,
794 )?,
795 );
796 self.save_metadata(&metadata).await?;
797
798 let uri = Self::path_to_uri(&path)?;
799 self.write_lance_batch_async(uri, batch).await?;
800 }
801 debug!("Subcentroids matrix saved successfully");
802 Ok(())
803 }
804
805 async fn load_subcentroids(&self) -> StorageResult<Vec<Vec<f64>>> {
807 let path = self.file_path("sub_centroids");
808 info!("Loading sub_centroids from {:?}", path);
809
810 let uri = Self::path_to_uri(&path)?;
811 let batch = self.read_lance_all_batches_async(uri).await?;
812 let matrix = self.from_dense_record_batch(&batch)?;
813
814 let (n_rows, n_cols) = matrix.shape();
816 let mut result = Vec::with_capacity(n_rows);
817
818 for row_idx in 0..n_rows {
819 let row: Vec<f64> = (0..n_cols)
820 .map(|col_idx| *matrix.get((row_idx, col_idx)))
821 .collect();
822 result.push(row);
823 }
824
825 info!(
826 "Loaded sub_centroids: {} x {} as Vec<Vec<f64>>",
827 n_rows, n_cols
828 );
829 Ok(result)
830 }
831
832 async fn save_item_norms(&self, item_norms: &[f64], md_path: &Path) -> StorageResult<()> {
834 info!("Saving {} item norm values", item_norms.len());
835 self.save_primitive_column::<Float64Type>(
836 "item_norms",
837 "norm",
838 item_norms.to_vec(),
839 md_path,
840 )
841 .await
842 }
843
844 async fn load_item_norms(&self) -> StorageResult<Vec<f64>> {
846 let path = self.file_path("item_norms");
847 info!("Loading item norms from {:?}", path);
848
849 let uri = Self::path_to_uri(&path)?;
850 let batch = self.read_lance_all_batches_async(uri).await?;
851 let arr = batch
852 .column(0)
853 .as_any()
854 .downcast_ref::<Float64Array>()
855 .ok_or_else(|| StorageError::Invalid("norm column type mismatch".into()))?;
856
857 let norms: Vec<f64> = (0..arr.len()).map(|i| arr.value(i)).collect();
858 info!("Loaded {} item norm values", norms.len());
859 Ok(norms)
860 }
861
862 async fn save_cluster_assignments(
863 &self,
864 assignments: &[Option<usize>],
865 md_path: &Path,
866 ) -> StorageResult<()> {
867 info!("Saving {} cluster assignments", assignments.len());
868
869 let values: Vec<i64> = assignments
871 .iter()
872 .map(|opt| opt.map(|v| v as i64).unwrap_or(-1))
873 .collect();
874
875 self.save_primitive_column::<Int64Type>(
876 "cluster_assignments",
877 "cluster_id",
878 values,
879 md_path,
880 )
881 .await
882 }
883
884 async fn load_cluster_assignments(&self) -> StorageResult<Vec<Option<usize>>> {
885 use arrow::array::Int64Array;
886 let path = self.file_path("cluster_assignments");
887 info!("Loading cluster assignments from {:?}", path);
888
889 let uri = Self::path_to_uri(&path)?;
890 let batch = self.read_lance_all_batches_async(uri).await?;
891 let arr = batch
892 .column(0)
893 .as_any()
894 .downcast_ref::<Int64Array>()
895 .ok_or_else(|| StorageError::Invalid("cluster_id column type mismatch".into()))?;
896
897 let assignments: Vec<Option<usize>> = (0..arr.len())
898 .map(|i| {
899 let v = arr.value(i);
900 if v < 0 { None } else { Some(v as usize) }
901 })
902 .collect();
903 info!("Loaded {} cluster assignments", assignments.len());
904 Ok(assignments)
905 }
906}