1use arrow::array::{Array as ArrowArray, FixedSizeListArray, Float64Array, UInt32Array};
2use arrow::datatypes::{DataType, Field, Schema};
3use arrow::record_batch::RecordBatch;
4use log::{debug, info, trace};
5use smartcore::linalg::basic::arrays::Array;
6use smartcore::linalg::basic::matrix::DenseMatrix;
7use sprs::{CsMat, TriMat};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11use crate::metadata::GeneMetadata;
12use crate::{StorageError, StorageResult};
13
14pub trait StorageBackend: Send + Sync {
71 fn get_base(&self) -> String;
73 fn get_name(&self) -> String;
75
76 fn exists(path: &str) -> (bool, Option<PathBuf>) {
81 let base_path = std::path::PathBuf::from(path);
82 if !base_path.exists() {
83 debug!("StorageBackend: path {:?} does not exist", base_path);
84 return (false, None);
85 }
86
87 if let Ok(entries) = std::fs::read_dir(&base_path) {
89 for entry in entries.flatten() {
90 let path = entry.path();
91 if let Some(name) = path.file_name().and_then(|n| n.to_str())
92 && name.ends_with("_metadata.json")
93 {
94 debug!("StorageBackend::exists: found metadata file at {:?}", path);
95 return (true, Some(path));
96 }
97 }
98 }
99 (false, None)
100 }
101
102 fn base_path(&self) -> PathBuf;
104 fn metadata_path(&self) -> PathBuf;
106 fn basepath_to_uri(&self) -> StorageResult<String>;
108
109 async fn load_dense_from_file(&self, path: &Path) -> StorageResult<DenseMatrix<f64>>;
118
119 fn file_path(&self, key: &str) -> PathBuf;
121
122 fn path_to_uri(path: &Path) -> StorageResult<String> {
131 let resolved = match path.canonicalize() {
132 Ok(canonical) => canonical,
133 Err(e) if e.kind() == std::io::ErrorKind::NotFound => path.to_path_buf(),
134 Err(e) => {
135 return Err(StorageError::Io(format!(
136 "Failed to resolve path `{}`: {}",
137 path.display(),
138 e
139 )));
140 }
141 };
142 if !resolved.is_absolute() {
143 return Err(StorageError::Invalid(format!(
144 "cannot convert relative path {:?} to a file:// URI; pass an absolute path",
145 path
146 )));
147 }
148 url::Url::from_file_path(&resolved)
149 .map(|u| u.to_string())
150 .map_err(|_| {
151 StorageError::Invalid(format!("cannot express {:?} as a file:// URI", resolved))
152 })
153 }
154
155 fn validate_initialized(&self, md_path: &Path) -> StorageResult<()> {
161 let expected = self.metadata_path();
162 if expected != *md_path {
163 return Err(StorageError::InvalidState(format!(
164 "metadata path mismatch: expected `{}`, found `{}`",
165 expected.display(),
166 md_path.display()
167 )));
168 }
169 if !md_path.exists() {
170 return Err(StorageError::Invalid(format!(
171 "Storage not initialized: metadata file missing at {:?}. \
172 Call save_metadata() or save_eigenmaps_all()/save_energymaps_all() first.",
173 md_path
174 )));
175 }
176 Ok(())
177 }
178
179 fn to_dense_record_batch(
192 &self,
193 matrix: &DenseMatrix<f64>,
194 ) -> Result<RecordBatch, StorageError> {
195 let (rows, cols) = (matrix.shape().0, matrix.shape().1);
196
197 debug!(
198 "Converting dense matrix to RecordBatch (vector format): {}x{}",
199 rows, cols
200 );
201
202 if rows == 0 || cols == 0 {
203 return Err(StorageError::Invalid(
204 "Cannot convert empty matrix to RecordBatch".to_string(),
205 ));
206 }
207
208 let mut values: Vec<f64> = Vec::with_capacity(rows * cols);
210 for r in 0..rows {
211 for c in 0..cols {
212 values.push(*matrix.get((r, c)));
213 }
214 }
215
216 let value_field = Field::new("item", DataType::Float64, false);
218 let list_field = Field::new(
219 "vector",
220 DataType::FixedSizeList(Arc::new(value_field), cols as i32),
221 false,
222 );
223
224 let schema = Schema::new(vec![list_field]);
225
226 let values_array = Float64Array::from(values);
228 let list_array = FixedSizeListArray::new(
229 Arc::new(Field::new("item", DataType::Float64, false)),
230 cols as i32,
231 Arc::new(values_array),
232 None, );
234
235 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(list_array)])
236 .map_err(|e| StorageError::Lance(e.to_string()))?;
237
238 trace!(
239 "RecordBatch created with {} rows (vectors of length {})",
240 batch.num_rows(),
241 cols
242 );
243
244 Ok(batch)
245 }
246
247 #[allow(clippy::wrong_self_convention)]
255 fn from_dense_record_batch(
256 &self,
257 batch: &RecordBatch,
258 ) -> Result<DenseMatrix<f64>, StorageError> {
259 use std::mem;
260
261 debug!("Reconstructing dense matrix from RecordBatch (vector format)");
262 debug!("Batch has {} columns", batch.num_columns());
263
264 if batch.num_columns() != 1 {
265 return Err(StorageError::Invalid(format!(
266 "Expected Lance row-major format with 1 FixedSizeList<Float64> column, but found {} columns. \
267 This parquet file appears to be in wide format (feature-per-column). \
268 Convert it first using: \
269 `python -c \"import pyarrow.parquet as pq; import pyarrow.compute as pc; \
270 tbl = pq.read_table('input.parquet'); \
271 import pyarrow as pa; \
272 vectors = pa.array([row.as_py() for row in tbl.to_pylist()], type=pa.list_(pa.float64(), len(tbl.column_names))); \
273 new_tbl = pa.table({{'vector': vectors}}); \
274 pq.write_table(new_tbl, 'output.parquet')\"` \
275 or use a Lance-native writer in your data pipeline.",
276 batch.num_columns()
277 )));
278 }
279
280 debug!("Extracting FixedSizeList column");
281 let column = batch.column(0);
282 let list_array = column
283 .as_any()
284 .downcast_ref::<FixedSizeListArray>()
285 .ok_or_else(|| {
286 StorageError::Invalid(format!(
287 "Column 0 is not FixedSizeList (found type: {:?}). \
288 Expected Lance row-major format with a single FixedSizeList<Float64> column.",
289 column.data_type()
290 ))
291 })?;
292
293 let rows = list_array.len();
294 let cols = list_array.value_length() as usize;
295
296 debug!("Matrix dimensions: {}x{}", rows, cols);
297
298 let total = rows
300 .checked_mul(cols)
301 .ok_or_else(|| StorageError::Invalid("Matrix size overflow (rows*cols)".to_string()))?;
302 let bytes = total
303 .checked_mul(mem::size_of::<f64>())
304 .ok_or_else(|| StorageError::Invalid("Byte size overflow".to_string()))?;
305
306 const MAX_BYTES: usize = 4usize * 1024 * 1024 * 1024; if bytes > MAX_BYTES {
308 return Err(StorageError::Invalid(format!(
309 "Dense load would allocate {} bytes for {}x{} matrix; exceeds 4GiB cap. \
310 Enable --reduce-dim or shard your input data.",
311 bytes, rows, cols
312 )));
313 }
314
315 let values_array = list_array
317 .values()
318 .as_any()
319 .downcast_ref::<Float64Array>()
320 .ok_or_else(|| {
321 StorageError::Invalid("FixedSizeList values are not Float64Array".to_string())
322 })?;
323
324 debug!("Converting row-major to column-major");
325 let mut data = vec![0.0f64; total];
326 for r in 0..rows {
327 for c in 0..cols {
328 let row_major_idx = r * cols + c;
329 let col_major_idx = c * rows + r;
330 data[col_major_idx] = values_array.value(row_major_idx);
331 }
332 }
333
334 debug!("Creating DenseMatrix");
335 DenseMatrix::new(rows, cols, data, true).map_err(|e| StorageError::Invalid(e.to_string()))
336 }
337
338 fn to_sparse_record_batch(&self, m: &CsMat<f64>) -> StorageResult<RecordBatch> {
342 debug!(
343 "Converting sparse matrix to RecordBatch: {} x {}, nnz={}",
344 m.rows(),
345 m.cols(),
346 m.nnz()
347 );
348
349 let mut row_idx = Vec::with_capacity(m.nnz());
350 let mut col_idx = Vec::with_capacity(m.nnz());
351 let mut vals = Vec::with_capacity(m.nnz());
352
353 for (v, (r, c)) in m.iter() {
354 row_idx.push(r as u32);
355 col_idx.push(c as u32);
356 vals.push(*v);
357 }
358
359 let mut schema_metadata = std::collections::HashMap::new();
361 schema_metadata.insert("rows".to_string(), m.rows().to_string());
362 schema_metadata.insert("cols".to_string(), m.cols().to_string());
363 schema_metadata.insert("nnz".to_string(), m.nnz().to_string());
364
365 let schema = Schema::new(vec![
366 Field::new("row", DataType::UInt32, false),
367 Field::new("col", DataType::UInt32, false),
368 Field::new("value", DataType::Float64, false),
369 ])
370 .with_metadata(schema_metadata);
371
372 let batch = RecordBatch::try_new(
373 Arc::new(schema),
374 vec![
375 Arc::new(UInt32Array::from(row_idx)) as _,
376 Arc::new(UInt32Array::from(col_idx)) as _,
377 Arc::new(Float64Array::from(vals)) as _,
378 ],
379 )
380 .map_err(|e| StorageError::Lance(e.to_string()))?;
381
382 trace!(
383 "Sparse RecordBatch created with {} entries",
384 batch.num_rows()
385 );
386 Ok(batch)
387 }
388
389 #[allow(clippy::wrong_self_convention)]
394 fn from_sparse_record_batch(
395 &self,
396 batch: RecordBatch,
397 expected_rows: usize,
398 expected_cols: usize,
399 ) -> StorageResult<CsMat<f64>> {
400 debug!("Reconstructing sparse matrix from RecordBatch");
401
402 let row_arr = batch
403 .column(0)
404 .as_any()
405 .downcast_ref::<UInt32Array>()
406 .ok_or_else(|| StorageError::Invalid("row column type mismatch".into()))?;
407 let col_arr = batch
408 .column(1)
409 .as_any()
410 .downcast_ref::<UInt32Array>()
411 .ok_or_else(|| StorageError::Invalid("col column type mismatch".into()))?;
412 let val_arr = batch
413 .column(2)
414 .as_any()
415 .downcast_ref::<Float64Array>()
416 .ok_or_else(|| StorageError::Invalid("value column type mismatch".into()))?;
417
418 let n = row_arr.len();
419 if n == 0 {
420 debug!(
421 "Empty RecordBatch, returning {}x{} sparse matrix",
422 expected_rows, expected_cols
423 );
424 return Ok(CsMat::zero((expected_rows, expected_cols)));
425 }
426
427 let schema = batch.schema();
429 let schema_metadata = schema.metadata();
430 if let (Some(rows_str), Some(cols_str)) =
431 (schema_metadata.get("rows"), schema_metadata.get("cols"))
432 {
433 let schema_rows = rows_str.parse::<usize>().ok();
434 let schema_cols = cols_str.parse::<usize>().ok();
435 if schema_rows != Some(expected_rows) || schema_cols != Some(expected_cols) {
436 return Err(StorageError::DimensionMismatch {
437 expected: format!("{}x{}", expected_rows, expected_cols),
438 found: match (schema_rows, schema_cols) {
439 (Some(r), Some(c)) => format!("{}x{}", r, c),
440 _ => format!(
441 "unparseable schema metadata {:?}x{:?}",
442 schema_rows, schema_cols
443 ),
444 },
445 });
446 } else {
447 debug!(
448 "Schema metadata matches storage metadata: {}x{}",
449 expected_rows, expected_cols
450 );
451 }
452 }
453
454 let rows = expected_rows;
455 let cols = expected_cols;
456 debug!(
457 "Reconstructing {}x{} sparse matrix from {} entries",
458 rows, cols, n
459 );
460
461 let mut trimat = TriMat::new((rows, cols));
462 for i in 0..n {
463 let r = row_arr.value(i) as usize;
464 let c = col_arr.value(i) as usize;
465 let v = val_arr.value(i);
466
467 if r >= rows || c >= cols {
468 return Err(StorageError::Invalid(format!(
469 "Index out of bounds: ({}, {}) in {}x{} matrix",
470 r, c, rows, cols
471 )));
472 }
473 trimat.add_triplet(r, c, v);
474 }
475
476 let result = trimat.to_csr();
477 if result.rows() != rows || result.cols() != cols {
478 return Err(StorageError::Invalid(format!(
479 "Dimension mismatch after reconstruction: expected {}x{}, got {}x{}",
480 rows,
481 cols,
482 result.rows(),
483 result.cols()
484 )));
485 }
486
487 Ok(result)
488 }
489
490 async fn save_dense(
492 &self,
493 key: &str,
494 matrix: &DenseMatrix<f64>,
495 md_path: &Path,
496 ) -> StorageResult<()>;
497
498 async fn load_dense(&self, key: &str) -> StorageResult<DenseMatrix<f64>>;
500
501 async fn save_sparse(
503 &self,
504 key: &str,
505 matrix: &CsMat<f64>,
506 md_path: &Path,
507 ) -> StorageResult<()>;
508
509 async fn load_sparse(&self, key: &str) -> StorageResult<CsMat<f64>>;
511
512 async fn save_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()>;
514
515 async fn load_lambdas(&self) -> StorageResult<Vec<f64>>;
517
518 async fn save_metadata(&self, metadata: &GeneMetadata) -> StorageResult<PathBuf> {
520 let path = self.metadata_path();
521 info!("Saving metadata to {:?}", path);
522 let s = serde_json::to_string_pretty(metadata).map_err(StorageError::Serde)?;
523 crate::generations::write_json_atomic(&path, &s)?;
526 info!("Metadata saved successfully");
527 Ok(path)
528 }
529
530 async fn load_metadata(&self) -> StorageResult<GeneMetadata> {
532 let filename = self.metadata_path();
533 info!("Loading metadata from {:?}", filename);
534 let s = tokio::fs::read_to_string(filename)
535 .await
536 .map_err(|e| StorageError::Io(e.to_string()))?;
537 let md: GeneMetadata = serde_json::from_str(&s).map_err(StorageError::Serde)?;
538 info!("Metadata loaded successfully");
539 Ok(md)
540 }
541
542 #[allow(dead_code)]
544 async fn save_index(&self, key: &str, vector: &[usize], md_path: &Path) -> StorageResult<()>;
545
546 async fn save_vector(&self, key: &str, vector: &[f64], md_path: &Path) -> StorageResult<()>;
548
549 async fn save_centroid_map(&self, map: &[usize], md_path: &Path) -> StorageResult<()>;
551
552 async fn load_centroid_map(&self) -> StorageResult<Vec<usize>>;
554 async fn save_subcentroid_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()>;
556 async fn load_subcentroid_lambdas(&self) -> StorageResult<Vec<f64>>;
558 async fn save_subcentroids(
560 &self,
561 subcentroids: &DenseMatrix<f64>,
562 md_path: &Path,
563 ) -> StorageResult<()>;
564 async fn load_subcentroids(&self) -> StorageResult<Vec<Vec<f64>>>;
566
567 async fn save_item_norms(&self, item_norms: &[f64], md_path: &Path) -> StorageResult<()>;
569
570 async fn load_item_norms(&self) -> StorageResult<Vec<f64>>;
572
573 async fn save_cluster_assignments(
575 &self,
576 assignments: &[Option<usize>],
577 md_path: &Path,
578 ) -> StorageResult<()>;
579
580 async fn load_cluster_assignments(&self) -> StorageResult<Vec<Option<usize>>>;
582
583 #[allow(dead_code)]
585 async fn load_index(&self, key: &str) -> StorageResult<Vec<usize>>;
586
587 async fn load_vector(&self, key: &str) -> StorageResult<Vec<f64>>;
588
589 async fn save_dense_to_file(data: &DenseMatrix<f64>, path: &Path) -> StorageResult<()>;
594}