use arrow_array::{Array, Int64Array, StringArray};
use cuttlefish_abi::Ty;
use cuttlefish_host::warehouse::{
bronze_batch, entry_for, silver_batch, write_manifest, write_parquet, Layer, Lineage, Manifest,
Row,
};
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
fn lineage() -> Lineage {
Lineage {
job_id: "job-1".into(),
spec_name: "index_corpus".into(),
spec_fingerprint: "abc123".into(),
model: "ollama:llama3.2:1b".into(),
embedding_model: Some("ollama:nomic-embed-text".into()),
cuttlefish_version: "0.8.0".into(),
}
}
fn rows() -> Vec<Row> {
vec![
Row {
node: "extract".into(),
item: 0,
status: "completed".into(),
concluded_at: "2026-08-18T00:00:00Z".into(),
source_input: Some(r#"{"path":"a.pdf"}"#.into()),
output: Some(
serde_json::json!({"title": "Annual Report", "pages": 227, "has_text": true}),
),
error: None,
},
Row {
node: "extract".into(),
item: 1,
status: "failed".into(),
concluded_at: "2026-08-18T00:00:01Z".into(),
source_input: Some(r#"{"path":"b.pdf"}"#.into()),
output: None,
error: Some("pdf has no text layer".into()),
},
]
}
fn read_back(path: &std::path::Path) -> Vec<arrow_array::RecordBatch> {
let file = std::fs::File::open(path).expect("the file must exist");
ParquetRecordBatchReaderBuilder::try_new(file)
.expect("the file must be readable Parquet")
.build()
.expect("building the reader")
.map(|b| b.expect("reading a batch"))
.collect()
}
#[test]
fn bronze_round_trips_through_a_real_parquet_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bronze/extract.parquet");
let batch = bronze_batch(&rows(), &lineage()).unwrap();
write_parquet(&path, &batch).unwrap();
let read = read_back(&path);
let total: usize = read.iter().map(|b| b.num_rows()).sum();
assert_eq!(total, 2, "both the success and the failure survive");
let b = &read[0];
let errors = b
.column_by_name("error")
.unwrap()
.as_any()
.downcast_ref::<StringArray>()
.expect("`error` reads back as a string column");
assert!(errors.is_null(0), "the successful item has no error");
assert_eq!(
errors.value(1),
"pdf has no text layer",
"the failure's reason survives the round trip verbatim"
);
let items = b
.column_by_name("item")
.unwrap()
.as_any()
.downcast_ref::<Int64Array>()
.expect("`item` reads back as an int column, not a stringified one");
assert_eq!((items.value(0), items.value(1)), (0, 1));
}
#[test]
fn a_silver_row_reads_back_as_the_value_not_its_json_encoding() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("silver/extract.parquet");
let ty = Ty::Record(
[
("title".to_string(), Ty::Text),
("pages".to_string(), Ty::Number),
("has_text".to_string(), Ty::Bool),
]
.into_iter()
.collect(),
);
let batch = silver_batch(&rows(), &lineage(), &ty).unwrap().unwrap();
write_parquet(&path, &batch).unwrap();
let read = read_back(&path);
let total: usize = read.iter().map(|b| b.num_rows()).sum();
assert_eq!(total, 1, "only the successful item reaches silver");
let titles = read[0]
.column_by_name("f_title")
.expect("the declared field is a column")
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
assert_eq!(titles.value(0), "Annual Report");
let jobs = read[0]
.column_by_name("job_id")
.expect("silver carries lineage")
.as_any()
.downcast_ref::<StringArray>()
.unwrap();
assert_eq!(jobs.value(0), "job-1");
}
#[test]
fn a_manifest_records_a_skipped_layer_with_its_reason() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("bronze/extract.parquet");
let batch = bronze_batch(&rows(), &lineage()).unwrap();
write_parquet(&path, &batch).unwrap();
let manifest = Manifest {
job_id: "job-1".into(),
spec_name: "index_corpus".into(),
spec_fingerprint: "abc123".into(),
model: "ollama:llama3.2:1b".into(),
embedding_model: None,
cuttlefish_version: "0.8.0".into(),
written_at: "2026-08-18T00:00:02Z".into(),
bronze: [(
"extract".to_string(),
Layer::Written(entry_for(dir.path(), &path, &batch)),
)]
.into_iter()
.collect(),
silver: [(
"extract".to_string(),
Layer::Skipped {
skipped: "node `extract` declares a Json output; there is no shape to validate"
.into(),
},
)]
.into_iter()
.collect(),
gold: Default::default(),
};
let written = write_manifest(dir.path(), &manifest).unwrap();
let text = std::fs::read_to_string(&written).unwrap();
let parsed: Manifest = serde_json::from_str(&text).expect("the manifest round trips");
match parsed.silver.get("extract").expect("silver is recorded") {
Layer::Skipped { skipped } => assert!(skipped.contains("Json"), "{skipped}"),
Layer::Written(_) => panic!("this layer was skipped, not written"),
}
match parsed.bronze.get("extract").expect("bronze is recorded") {
Layer::Written(entry) => {
assert_eq!(entry.rows, 2);
assert_eq!(entry.path, "bronze/extract.parquet");
assert!(!entry.path.starts_with('/'), "{}", entry.path);
}
Layer::Skipped { .. } => panic!("bronze was written"),
}
}
#[test]
fn emit_a_fixture_warehouse_when_asked() {
let Ok(out) = std::env::var("CUTTLEFISH_WAREHOUSE_OUT") else {
return;
};
let root = std::path::PathBuf::from(out);
let ty = Ty::Record(
[
("title".to_string(), Ty::Text),
("pages".to_string(), Ty::Number),
("has_text".to_string(), Ty::Bool),
]
.into_iter()
.collect(),
);
let bronze = bronze_batch(&rows(), &lineage()).unwrap();
let bronze_path = root.join("bronze/extract.parquet");
write_parquet(&bronze_path, &bronze).unwrap();
let silver = silver_batch(&rows(), &lineage(), &ty).unwrap().unwrap();
let silver_path = root.join("silver/extract.parquet");
write_parquet(&silver_path, &silver).unwrap();
let manifest = Manifest {
job_id: "job-1".into(),
spec_name: "index_corpus".into(),
spec_fingerprint: "abc123".into(),
model: "ollama:llama3.2:1b".into(),
embedding_model: Some("ollama:nomic-embed-text".into()),
cuttlefish_version: "0.8.0".into(),
written_at: "2026-08-18T00:00:02Z".into(),
bronze: [(
"extract".to_string(),
Layer::Written(entry_for(&root, &bronze_path, &bronze)),
)]
.into_iter()
.collect(),
silver: [(
"extract".to_string(),
Layer::Written(entry_for(&root, &silver_path, &silver)),
)]
.into_iter()
.collect(),
gold: Default::default(),
};
write_manifest(&root, &manifest).unwrap();
}