genegraph_storage/traits/
backend.rs1use 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::fs;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11
12use crate::metadata::GeneMetadata;
13use crate::{StorageError, StorageResult};
14
15pub trait StorageBackend: Send + Sync {
72 fn get_base(&self) -> String;
74 fn get_name(&self) -> String;
76
77 fn exists(path: &str) -> (bool, Option<PathBuf>) {
82 let base_path = std::path::PathBuf::from(path);
83 if !base_path.exists() {
84 debug!("StorageBackend: path {:?} does not exist", base_path);
85 return (false, None);
86 }
87
88 if let Ok(entries) = std::fs::read_dir(&base_path) {
90 for entry in entries.flatten() {
91 let path = entry.path();
92 if let Some(name) = path.file_name().and_then(|n| n.to_str())
93 && name.ends_with("_metadata.json")
94 {
95 debug!("StorageBackend::exists: found metadata file at {:?}", path);
96 return (true, Some(path));
97 }
98 }
99 }
100 (false, None)
101 }
102
103 fn base_path(&self) -> PathBuf;
105 fn metadata_path(&self) -> PathBuf;
107 fn basepath_to_uri(&self) -> StorageResult<String>;
109
110 async fn load_dense_from_file(&self, path: &Path) -> StorageResult<DenseMatrix<f64>>;
113
114 fn file_path(&self, key: &str) -> PathBuf;
116
117 fn path_to_uri(path: &Path) -> StorageResult<String> {
124 let resolved = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
125 if !resolved.is_absolute() {
126 return Err(StorageError::Invalid(format!(
127 "cannot convert relative path {:?} to a file:// URI; pass an absolute path",
128 path
129 )));
130 }
131 url::Url::from_file_path(&resolved)
132 .map(|u| u.to_string())
133 .map_err(|_| {
134 StorageError::Invalid(format!("cannot express {:?} as a file:// URI", resolved))
135 })
136 }
137
138 fn validate_initialized(&self, md_path: &Path) -> StorageResult<()> {
144 assert_eq!(self.metadata_path(), *md_path);
145 if !md_path.exists() {
146 return Err(StorageError::Invalid(format!(
147 "Storage not initialized: metadata file missing at {:?}. \
148 Call save_metadata() or save_eigenmaps_all()/save_energymaps_all() first.",
149 md_path
150 )));
151 }
152 Ok(())
153 }
154
155 fn to_dense_record_batch(
168 &self,
169 matrix: &DenseMatrix<f64>,
170 ) -> Result<RecordBatch, StorageError> {
171 let (rows, cols) = (matrix.shape().0, matrix.shape().1);
172
173 debug!(
174 "Converting dense matrix to RecordBatch (vector format): {}x{}",
175 rows, cols
176 );
177
178 if rows == 0 || cols == 0 {
179 return Err(StorageError::Invalid(
180 "Cannot convert empty matrix to RecordBatch".to_string(),
181 ));
182 }
183
184 let mut values: Vec<f64> = Vec::with_capacity(rows * cols);
186 for r in 0..rows {
187 for c in 0..cols {
188 values.push(*matrix.get((r, c)));
189 }
190 }
191
192 let value_field = Field::new("item", DataType::Float64, false);
194 let list_field = Field::new(
195 "vector",
196 DataType::FixedSizeList(Arc::new(value_field), cols as i32),
197 false,
198 );
199
200 let schema = Schema::new(vec![list_field]);
201
202 let values_array = Float64Array::from(values);
204 let list_array = FixedSizeListArray::new(
205 Arc::new(Field::new("item", DataType::Float64, false)),
206 cols as i32,
207 Arc::new(values_array),
208 None, );
210
211 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(list_array)])
212 .map_err(|e| StorageError::Lance(e.to_string()))?;
213
214 trace!(
215 "RecordBatch created with {} rows (vectors of length {})",
216 batch.num_rows(),
217 cols
218 );
219
220 Ok(batch)
221 }
222
223 #[allow(clippy::wrong_self_convention)]
231 fn from_dense_record_batch(
232 &self,
233 batch: &RecordBatch,
234 ) -> Result<DenseMatrix<f64>, StorageError> {
235 use std::mem;
236
237 debug!("Reconstructing dense matrix from RecordBatch (vector format)");
238 debug!("Batch has {} columns", batch.num_columns());
239
240 if batch.num_columns() != 1 {
241 return Err(StorageError::Invalid(format!(
242 "Expected Lance row-major format with 1 FixedSizeList<Float64> column, but found {} columns. \
243 This parquet file appears to be in wide format (feature-per-column). \
244 Convert it first using: \
245 `python -c \"import pyarrow.parquet as pq; import pyarrow.compute as pc; \
246 tbl = pq.read_table('input.parquet'); \
247 import pyarrow as pa; \
248 vectors = pa.array([row.as_py() for row in tbl.to_pylist()], type=pa.list_(pa.float64(), len(tbl.column_names))); \
249 new_tbl = pa.table({{'vector': vectors}}); \
250 pq.write_table(new_tbl, 'output.parquet')\"` \
251 or use a Lance-native writer in your data pipeline.",
252 batch.num_columns()
253 )));
254 }
255
256 debug!("Extracting FixedSizeList column");
257 let column = batch.column(0);
258 let list_array = column
259 .as_any()
260 .downcast_ref::<FixedSizeListArray>()
261 .ok_or_else(|| {
262 StorageError::Invalid(format!(
263 "Column 0 is not FixedSizeList (found type: {:?}). \
264 Expected Lance row-major format with a single FixedSizeList<Float64> column.",
265 column.data_type()
266 ))
267 })?;
268
269 let rows = list_array.len();
270 let cols = list_array.value_length() as usize;
271
272 debug!("Matrix dimensions: {}x{}", rows, cols);
273
274 let total = rows
276 .checked_mul(cols)
277 .ok_or_else(|| StorageError::Invalid("Matrix size overflow (rows*cols)".to_string()))?;
278 let bytes = total
279 .checked_mul(mem::size_of::<f64>())
280 .ok_or_else(|| StorageError::Invalid("Byte size overflow".to_string()))?;
281
282 const MAX_BYTES: usize = 4usize * 1024 * 1024 * 1024; if bytes > MAX_BYTES {
284 return Err(StorageError::Invalid(format!(
285 "Dense load would allocate {} bytes for {}x{} matrix; exceeds 4GiB cap. \
286 Enable --reduce-dim or shard your input data.",
287 bytes, rows, cols
288 )));
289 }
290
291 let values_array = list_array
293 .values()
294 .as_any()
295 .downcast_ref::<Float64Array>()
296 .ok_or_else(|| {
297 StorageError::Invalid("FixedSizeList values are not Float64Array".to_string())
298 })?;
299
300 debug!("Converting row-major to column-major");
301 let mut data = vec![0.0f64; total];
302 for r in 0..rows {
303 for c in 0..cols {
304 let row_major_idx = r * cols + c;
305 let col_major_idx = c * rows + r;
306 data[col_major_idx] = values_array.value(row_major_idx);
307 }
308 }
309
310 debug!("Creating DenseMatrix");
311 DenseMatrix::new(rows, cols, data, true).map_err(|e| StorageError::Invalid(e.to_string()))
312 }
313
314 fn to_sparse_record_batch(&self, m: &CsMat<f64>) -> StorageResult<RecordBatch> {
318 debug!(
319 "Converting sparse matrix to RecordBatch: {} x {}, nnz={}",
320 m.rows(),
321 m.cols(),
322 m.nnz()
323 );
324
325 let mut row_idx = Vec::with_capacity(m.nnz());
326 let mut col_idx = Vec::with_capacity(m.nnz());
327 let mut vals = Vec::with_capacity(m.nnz());
328
329 for (v, (r, c)) in m.iter() {
330 row_idx.push(r as u32);
331 col_idx.push(c as u32);
332 vals.push(*v);
333 }
334
335 let mut schema_metadata = std::collections::HashMap::new();
337 schema_metadata.insert("rows".to_string(), m.rows().to_string());
338 schema_metadata.insert("cols".to_string(), m.cols().to_string());
339 schema_metadata.insert("nnz".to_string(), m.nnz().to_string());
340
341 let schema = Schema::new(vec![
342 Field::new("row", DataType::UInt32, false),
343 Field::new("col", DataType::UInt32, false),
344 Field::new("value", DataType::Float64, false),
345 ])
346 .with_metadata(schema_metadata);
347
348 let batch = RecordBatch::try_new(
349 Arc::new(schema),
350 vec![
351 Arc::new(UInt32Array::from(row_idx)) as _,
352 Arc::new(UInt32Array::from(col_idx)) as _,
353 Arc::new(Float64Array::from(vals)) as _,
354 ],
355 )
356 .map_err(|e| StorageError::Lance(e.to_string()))?;
357
358 trace!(
359 "Sparse RecordBatch created with {} entries",
360 batch.num_rows()
361 );
362 Ok(batch)
363 }
364
365 #[allow(clippy::wrong_self_convention)]
370 fn from_sparse_record_batch(
371 &self,
372 batch: RecordBatch,
373 expected_rows: usize,
374 expected_cols: usize,
375 ) -> StorageResult<CsMat<f64>> {
376 debug!("Reconstructing sparse matrix from RecordBatch");
377
378 let row_arr = batch
379 .column(0)
380 .as_any()
381 .downcast_ref::<UInt32Array>()
382 .ok_or_else(|| StorageError::Invalid("row column type mismatch".into()))?;
383 let col_arr = batch
384 .column(1)
385 .as_any()
386 .downcast_ref::<UInt32Array>()
387 .ok_or_else(|| StorageError::Invalid("col column type mismatch".into()))?;
388 let val_arr = batch
389 .column(2)
390 .as_any()
391 .downcast_ref::<Float64Array>()
392 .ok_or_else(|| StorageError::Invalid("value column type mismatch".into()))?;
393
394 let n = row_arr.len();
395 if n == 0 {
396 debug!(
397 "Empty RecordBatch, returning {}x{} sparse matrix",
398 expected_rows, expected_cols
399 );
400 return Ok(CsMat::zero((expected_rows, expected_cols)));
401 }
402
403 let schema = batch.schema();
405 let schema_metadata = schema.metadata();
406 if let (Some(rows_str), Some(cols_str)) =
407 (schema_metadata.get("rows"), schema_metadata.get("cols"))
408 {
409 let schema_rows = rows_str.parse::<usize>().ok();
410 let schema_cols = cols_str.parse::<usize>().ok();
411 if schema_rows != Some(expected_rows) || schema_cols != Some(expected_cols) {
412 panic!(
413 "Schema metadata dimensions ({:?}x{:?}) don't match storage metadata ({}x{})",
414 schema_rows, schema_cols, expected_rows, expected_cols
415 );
416 } else {
417 debug!(
418 "Schema metadata matches storage metadata: {}x{}",
419 expected_rows, expected_cols
420 );
421 }
422 }
423
424 let rows = expected_rows;
425 let cols = expected_cols;
426 debug!(
427 "Reconstructing {}x{} sparse matrix from {} entries",
428 rows, cols, n
429 );
430
431 let mut trimat = TriMat::new((rows, cols));
432 for i in 0..n {
433 let r = row_arr.value(i) as usize;
434 let c = col_arr.value(i) as usize;
435 let v = val_arr.value(i);
436
437 if r >= rows || c >= cols {
438 return Err(StorageError::Invalid(format!(
439 "Index out of bounds: ({}, {}) in {}x{} matrix",
440 r, c, rows, cols
441 )));
442 }
443 trimat.add_triplet(r, c, v);
444 }
445
446 let result = trimat.to_csr();
447 if result.rows() != rows || result.cols() != cols {
448 return Err(StorageError::Invalid(format!(
449 "Dimension mismatch after reconstruction: expected {}x{}, got {}x{}",
450 rows,
451 cols,
452 result.rows(),
453 result.cols()
454 )));
455 }
456
457 Ok(result)
458 }
459
460 async fn save_dense(
462 &self,
463 key: &str,
464 matrix: &DenseMatrix<f64>,
465 md_path: &Path,
466 ) -> StorageResult<()>;
467
468 async fn load_dense(&self, key: &str) -> StorageResult<DenseMatrix<f64>>;
470
471 async fn save_sparse(
473 &self,
474 key: &str,
475 matrix: &CsMat<f64>,
476 md_path: &Path,
477 ) -> StorageResult<()>;
478
479 async fn load_sparse(&self, key: &str) -> StorageResult<CsMat<f64>>;
481
482 async fn save_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()>;
484
485 async fn load_lambdas(&self) -> StorageResult<Vec<f64>>;
487
488 async fn save_metadata(&self, metadata: &GeneMetadata) -> StorageResult<PathBuf> {
490 let path = self.metadata_path();
491 info!("Saving metadata to {:?}", path);
492 fs::create_dir_all(self.base_path()).map_err(|e| StorageError::Io(e.to_string()))?;
493 let s = serde_json::to_string_pretty(metadata).map_err(StorageError::Serde)?;
494 fs::write(&path, s).map_err(|e| StorageError::Io(e.to_string()))?;
495 info!("Metadata saved successfully");
496 Ok(path)
497 }
498
499 async fn load_metadata(&self) -> StorageResult<GeneMetadata> {
501 let filename = self.metadata_path();
502 info!("Loading metadata from {:?}", filename);
503 let s = fs::read_to_string(filename).map_err(|e| StorageError::Io(e.to_string()))?;
504 let md: GeneMetadata = serde_json::from_str(&s).map_err(StorageError::Serde)?;
505 info!("Metadata loaded successfully");
506 Ok(md)
507 }
508
509 #[allow(dead_code)]
511 async fn save_index(&self, key: &str, vector: &[usize], md_path: &Path) -> StorageResult<()>;
512
513 async fn save_vector(&self, key: &str, vector: &[f64], md_path: &Path) -> StorageResult<()>;
515
516 async fn save_centroid_map(&self, map: &[usize], md_path: &Path) -> StorageResult<()>;
518
519 async fn load_centroid_map(&self) -> StorageResult<Vec<usize>>;
521 async fn save_subcentroid_lambdas(&self, lambdas: &[f64], md_path: &Path) -> StorageResult<()>;
523 async fn load_subcentroid_lambdas(&self) -> StorageResult<Vec<f64>>;
525 async fn save_subcentroids(
527 &self,
528 subcentroids: &DenseMatrix<f64>,
529 md_path: &Path,
530 ) -> StorageResult<()>;
531 async fn load_subcentroids(&self) -> StorageResult<Vec<Vec<f64>>>;
533
534 async fn save_item_norms(&self, item_norms: &[f64], md_path: &Path) -> StorageResult<()>;
536
537 async fn load_item_norms(&self) -> StorageResult<Vec<f64>>;
539
540 async fn save_cluster_assignments(
542 &self,
543 assignments: &[Option<usize>],
544 md_path: &Path,
545 ) -> StorageResult<()>;
546
547 async fn load_cluster_assignments(&self) -> StorageResult<Vec<Option<usize>>>;
549
550 #[allow(dead_code)]
552 async fn load_index(&self, key: &str) -> StorageResult<Vec<usize>>;
553
554 async fn load_vector(&self, key: &str) -> StorageResult<Vec<f64>>;
555
556 async fn save_dense_to_file(data: &DenseMatrix<f64>, path: &Path) -> StorageResult<()>;
557}