1use anyhow::{Context, Result};
30use extsort_iter::*;
31use std::cmp::Ordering;
32use std::fs::File;
33use std::io::{BufRead, BufReader, BufWriter, Seek, SeekFrom, Write};
34use std::path::Path;
35
36const MAGIC: &[u8; 8] = b"FLANKDB\0";
37const VERSION: u32 = 2; #[derive(Clone)]
42pub struct FlankingRecord {
43 pub gene: String,
45 pub line: String,
47}
48
49impl PartialEq for FlankingRecord {
50 fn eq(&self, other: &Self) -> bool {
51 self.gene == other.gene
52 }
53}
54
55impl Eq for FlankingRecord {}
56
57impl PartialOrd for FlankingRecord {
58 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
59 Some(self.cmp(other))
60 }
61}
62
63impl Ord for FlankingRecord {
64 fn cmp(&self, other: &Self) -> Ordering {
65 self.gene.cmp(&other.gene)
66 }
67}
68
69pub fn build(
93 tsv_path: &Path,
94 fdb_path: &Path,
95 buffer_size_mb: usize,
96 threads: usize,
97) -> Result<()> {
98 eprintln!("Building FDB from TSV...");
99 eprintln!(" Input: {}", tsv_path.display());
100 eprintln!(" Output: {}", fdb_path.display());
101 eprintln!(" Buffer: {} MB, Threads: {}", buffer_size_mb, threads);
102
103 rayon::ThreadPoolBuilder::new()
105 .num_threads(threads)
106 .build_global()
107 .ok(); let temp_dir = tempfile::Builder::new()
111 .prefix("fdb_sort_")
112 .tempdir()
113 .context("Failed to create temp directory")?;
114
115 eprintln!(" Temp dir: {}", temp_dir.path().display());
116
117 eprintln!("\n[Phase 1] Reading TSV and external sorting...");
119
120 let file = File::open(tsv_path).context("Failed to open TSV file")?;
121 let file_size = file.metadata()?.len();
122 let reader = BufReader::with_capacity(8 * 1024 * 1024, file);
123 let mut lines = reader.lines();
124
125 let header = lines
127 .next()
128 .ok_or_else(|| anyhow::anyhow!("Empty TSV file"))??;
129
130 let record_iter = lines.filter_map(|line_result| {
132 let line = line_result.ok()?;
133 if line.is_empty() {
134 return None;
135 }
136 let gene = line.split('\t').next()?.to_string();
138 if gene.is_empty() {
139 return None;
140 }
141 Some(FlankingRecord { gene, line })
142 });
143
144 let buffer_bytes = buffer_size_mb * 1024 * 1024;
147 let config = ExtsortConfig::with_buffer_size(buffer_bytes)
148 .compress_lz4_flex();
149
150 let sorted_iter = record_iter
152 .par_external_sort(config)
153 .context("External sort failed")?;
154
155 eprintln!("[Phase 2] Building FDB from sorted records...");
157
158 let mut output = BufWriter::with_capacity(4 * 1024 * 1024, File::create(fdb_path)?);
159
160 output.write_all(MAGIC)?;
162 output.write_all(&VERSION.to_le_bytes())?;
163 output.write_all(&0u32.to_le_bytes())?; output.write_all(&0u64.to_le_bytes())?; let mut index_entries: Vec<(String, u64, u32, u32)> = Vec::new();
168 let mut compressor = zstd::bulk::Compressor::new(3)?;
169
170 let mut current_gene: Option<String> = None;
171 let mut current_records: Vec<String> = Vec::new();
172 let mut total_records = 0u64;
173 let mut gene_count = 0u32;
174
175 for record in sorted_iter {
176 total_records += 1;
177
178 if current_gene.as_ref() != Some(&record.gene) {
179 if let Some(prev_gene) = current_gene.take() {
181 write_gene_block(
182 &mut output,
183 &mut compressor,
184 &header,
185 &prev_gene,
186 ¤t_records,
187 &mut index_entries,
188 )?;
189 gene_count += 1;
190
191 if gene_count.is_multiple_of(1000) {
192 eprintln!(
193 " Processed {} genes, {} records...",
194 gene_count, total_records
195 );
196 }
197 }
198
199 current_gene = Some(record.gene);
200 current_records.clear();
201 }
202
203 current_records.push(record.line);
204 }
205
206 if let Some(gene) = current_gene {
208 write_gene_block(
209 &mut output,
210 &mut compressor,
211 &header,
212 &gene,
213 ¤t_records,
214 &mut index_entries,
215 )?;
216 gene_count += 1;
217 }
218
219 eprintln!(" Total: {} genes, {} records", gene_count, total_records);
220
221 eprintln!("[Phase 3] Writing index...");
223
224 let index_offset = output.stream_position()?;
225
226 for (gene, offset, comp_len, record_count) in &index_entries {
227 let gene_bytes = gene.as_bytes();
228 output.write_all(&(gene_bytes.len() as u16).to_le_bytes())?;
229 output.write_all(gene_bytes)?;
230 output.write_all(&offset.to_le_bytes())?;
231 output.write_all(&comp_len.to_le_bytes())?;
232 output.write_all(&record_count.to_le_bytes())?;
233 }
234
235 output.seek(SeekFrom::Start(12))?;
237 output.write_all(&gene_count.to_le_bytes())?;
238 output.write_all(&index_offset.to_le_bytes())?;
239 output.flush()?;
240
241 drop(temp_dir);
243
244 let input_size = file_size;
246 let output_size = std::fs::metadata(fdb_path)?.len();
247 let ratio = input_size as f64 / output_size as f64;
248
249 eprintln!("\n=== FDB Build Complete ===");
250 eprintln!(" Input: {:.2} MB", input_size as f64 / 1024.0 / 1024.0);
251 eprintln!(" Output: {:.2} MB", output_size as f64 / 1024.0 / 1024.0);
252 eprintln!(" Compression ratio: {:.1}x", ratio);
253 eprintln!(" Genes: {}", gene_count);
254 eprintln!(" Records: {}", total_records);
255
256 Ok(())
257}
258
259pub fn build_from_sorted(
268 tsv_path: &Path,
269 fdb_path: &Path,
270) -> Result<()> {
271 eprintln!("Building FDB from pre-sorted TSV (streaming mode)...");
272 eprintln!(" Input: {}", tsv_path.display());
273 eprintln!(" Output: {}", fdb_path.display());
274
275 let file = File::open(tsv_path).context("Failed to open TSV file")?;
276 let file_size = file.metadata()?.len();
277 let reader = BufReader::with_capacity(8 * 1024 * 1024, file);
278 let mut lines = reader.lines();
279
280 let header = lines
282 .next()
283 .ok_or_else(|| anyhow::anyhow!("Empty TSV file"))??;
284
285 let mut output = BufWriter::with_capacity(4 * 1024 * 1024, File::create(fdb_path)?);
287
288 output.write_all(MAGIC)?;
290 output.write_all(&VERSION.to_le_bytes())?;
291 output.write_all(&0u32.to_le_bytes())?; output.write_all(&0u64.to_le_bytes())?; let mut index_entries: Vec<(String, u64, u32, u32)> = Vec::new();
295 let mut compressor = zstd::bulk::Compressor::new(3)?;
296
297 let mut current_gene: Option<String> = None;
298 let mut current_records: Vec<String> = Vec::new();
299 let mut total_records = 0u64;
300 let mut gene_count = 0u32;
301
302 eprintln!("\n[Streaming] Processing sorted TSV...");
303
304 for line_result in lines {
305 let line = match line_result {
306 Ok(l) => l,
307 Err(_) => continue,
308 };
309 if line.is_empty() {
310 continue;
311 }
312
313 let gene = match line.split('\t').next() {
315 Some(g) if !g.is_empty() => g.to_string(),
316 _ => continue,
317 };
318
319 total_records += 1;
320
321 if current_gene.as_ref() != Some(&gene) {
323 if let Some(prev_gene) = current_gene.take() {
325 write_gene_block(
326 &mut output,
327 &mut compressor,
328 &header,
329 &prev_gene,
330 ¤t_records,
331 &mut index_entries,
332 )?;
333 gene_count += 1;
334
335 if gene_count % 1000 == 0 {
336 eprintln!(
337 " Processed {} genes, {} records...",
338 gene_count, total_records
339 );
340 }
341 }
342
343 current_gene = Some(gene);
344 current_records.clear();
345 }
346
347 current_records.push(line);
348 }
349
350 if let Some(gene) = current_gene {
352 write_gene_block(
353 &mut output,
354 &mut compressor,
355 &header,
356 &gene,
357 ¤t_records,
358 &mut index_entries,
359 )?;
360 gene_count += 1;
361 }
362
363 eprintln!(" Total: {} genes, {} records", gene_count, total_records);
364
365 eprintln!("[Writing index]");
367 let index_offset = output.stream_position()?;
368
369 for (gene, offset, comp_len, record_count) in &index_entries {
370 let gene_bytes = gene.as_bytes();
371 output.write_all(&(gene_bytes.len() as u16).to_le_bytes())?;
372 output.write_all(gene_bytes)?;
373 output.write_all(&offset.to_le_bytes())?;
374 output.write_all(&comp_len.to_le_bytes())?;
375 output.write_all(&record_count.to_le_bytes())?;
376 }
377
378 output.seek(SeekFrom::Start(12))?;
380 output.write_all(&gene_count.to_le_bytes())?;
381 output.write_all(&index_offset.to_le_bytes())?;
382 output.flush()?;
383
384 let output_size = std::fs::metadata(fdb_path)?.len();
386 let ratio = file_size as f64 / output_size as f64;
387
388 eprintln!("\n=== FDB Build Complete (Streaming) ===");
389 eprintln!(" Input: {:.2} MB", file_size as f64 / 1024.0 / 1024.0);
390 eprintln!(" Output: {:.2} MB", output_size as f64 / 1024.0 / 1024.0);
391 eprintln!(" Compression ratio: {:.1}x", ratio);
392 eprintln!(" Genes: {}", gene_count);
393 eprintln!(" Records: {}", total_records);
394
395 Ok(())
396}
397
398fn write_gene_block(
400 output: &mut BufWriter<File>,
401 compressor: &mut zstd::bulk::Compressor<'_>,
402 header: &str,
403 gene: &str,
404 records: &[String],
405 index_entries: &mut Vec<(String, u64, u32, u32)>,
406) -> Result<()> {
407 let mut content = String::with_capacity(records.len() * 3000);
409 content.push_str(header);
410 content.push('\n');
411 for record in records {
412 content.push_str(record);
413 content.push('\n');
414 }
415
416 let compressed = compressor.compress(content.as_bytes())?;
418 let offset = output.stream_position()?;
419 output.write_all(&compressed)?;
420
421 index_entries.push((
423 gene.to_string(),
424 offset,
425 compressed.len() as u32,
426 records.len() as u32,
427 ));
428
429 Ok(())
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn test_flanking_record_ordering() {
438 let r1 = FlankingRecord {
439 gene: "aac(6')".to_string(),
440 line: "test1".to_string(),
441 };
442 let r2 = FlankingRecord {
443 gene: "blaTEM".to_string(),
444 line: "test2".to_string(),
445 };
446 assert!(r1 < r2);
447 }
448}