use std::collections::BTreeMap;
use arrow_array::RecordBatch;
use crate::{
error::Result, mapping::MappingSpec, mutation::GraphMutation, registry::TransformRegistry,
transform::TableContext,
};
pub trait TableSource {
fn batches(&self, table: &str) -> &[RecordBatch];
}
pub fn build_mutation(spec: &MappingSpec, source: &dyn TableSource) -> Result<GraphMutation> {
build_mutation_reported(spec, source).map(|(m, _)| m)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BuildReport {
pub tables_mapped: Vec<String>,
pub tables_empty: Vec<String>,
pub rows_read: usize,
pub vertices: usize,
pub edges: usize,
}
impl BuildReport {
pub fn require_non_empty(self) -> Result<Self> {
if self.rows_read == 0 || (self.vertices == 0 && self.edges == 0) {
return Err(crate::error::BifrostError::Source(format!(
"the rel2graph build read {} row(s) from {} table(s) and produced {} vertex \
row(s) + {} edge row(s) — that is an EMPTY graph, not a built one. Tables \
the source carried nothing for: {:?}. Check the catalog, the namespace and \
the table names in the mapping spec.",
self.rows_read,
self.tables_mapped.len(),
self.vertices,
self.edges,
self.tables_empty,
)));
}
Ok(self)
}
}
pub fn build_mutation_reported(
spec: &MappingSpec,
source: &dyn TableSource,
) -> Result<(GraphMutation, BuildReport)> {
let registry = TransformRegistry::new();
let mut mutation = GraphMutation::default();
let mut report = BuildReport::default();
for tm in &spec.tables {
let batches = source.batches(&tm.table);
let rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
if batches.is_empty() || rows == 0 {
report.tables_empty.push(tm.table.clone());
continue;
}
report.tables_mapped.push(tm.table.clone());
report.rows_read += rows;
let ctx = TableContext {
table: tm.table.clone(),
};
let transforms = tm.compile(®istry)?;
for transform in &transforms {
let m = transform.apply(&ctx, batches)?;
mutation.merge(m);
}
}
report.vertices = mutation.vertices.iter().map(|s| s.batch.num_rows()).sum();
report.edges = mutation.edges.iter().map(|s| s.batch.num_rows()).sum();
Ok((mutation, report))
}
#[derive(Debug, Clone, Default)]
pub struct ArrowTables {
tables: BTreeMap<String, Vec<RecordBatch>>,
}
impl ArrowTables {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, table: impl Into<String>, batches: Vec<RecordBatch>) {
self.tables.insert(table.into(), batches);
}
pub fn with_table(mut self, table: impl Into<String>, batches: Vec<RecordBatch>) -> Self {
self.insert(table, batches);
self
}
pub fn table_names(&self) -> impl Iterator<Item = &str> {
self.tables.keys().map(String::as_str)
}
pub fn is_empty(&self) -> bool {
self.tables.is_empty()
}
pub fn len(&self) -> usize {
self.tables.len()
}
}
impl TableSource for ArrowTables {
fn batches(&self, table: &str) -> &[RecordBatch] {
self.tables.get(table).map(Vec::as_slice).unwrap_or(&[])
}
}
impl<T: TableSource + ?Sized> TableSource for &T {
fn batches(&self, table: &str) -> &[RecordBatch] {
(**self).batches(table)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::Int64Array;
use arrow_schema::{DataType, Field, Schema};
use std::sync::Arc;
fn id_batch(col: &str, ids: &[i64]) -> RecordBatch {
let schema = Arc::new(Schema::new(vec![Field::new(col, DataType::Int64, false)]));
RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(ids.to_vec()))]).unwrap()
}
#[test]
fn arrow_tables_source_drives_the_engine() {
let spec = MappingSpec::from_yaml(
r#"
version: 1
tables:
- table: lake.hr.employees
pipeline:
- vertex: { label: Employee, id_column: employee_id, properties: [] }
"#,
)
.unwrap();
let src = ArrowTables::new().with_table(
"lake.hr.employees",
vec![id_batch("employee_id", &[1, 2, 3])],
);
assert_eq!(src.len(), 1);
let mutation = build_mutation(&spec, &src).unwrap();
assert_eq!(
mutation.vertices.len(),
1,
"one VertexSet for the one table"
);
assert_eq!(mutation.vertices[0].label, "Employee");
assert_eq!(mutation.vertices[0].batch.num_rows(), 3);
}
#[test]
fn missing_tables_are_skipped_not_errors() {
let spec = MappingSpec::from_yaml(
r#"
version: 1
tables:
- table: present.table
pipeline:
- vertex: { label: A, id_column: id, properties: [] }
- table: absent.table
pipeline:
- vertex: { label: B, id_column: id, properties: [] }
"#,
)
.unwrap();
let src = ArrowTables::new().with_table("present.table", vec![id_batch("id", &[10, 20])]);
let mutation = build_mutation(&spec, &src).unwrap();
assert_eq!(
mutation.vertices.len(),
1,
"absent table skipped, present table mapped"
);
assert_eq!(mutation.vertices[0].label, "A");
}
}