use std::collections::HashMap;
use std::sync::Arc;
use arrow_array::RecordBatch;
use arrow_schema::{Schema, SchemaRef};
use vgi_rpc::{Result, RpcError};
use crate::cache_control::CacheControl;
use crate::function::{ArgSpec, BindParams, BindResponse, FunctionMetadata, ProcessParams};
pub const PARENT_ROW_METADATA_KEY: &str = "vgi_rpc.parent_row#b64";
#[derive(Default)]
pub struct EmitOptions {
pub metadata: Option<HashMap<String, String>>,
pub cache_control: Option<CacheControl>,
pub parent_rows: Option<Vec<i32>>,
}
#[derive(Default)]
pub struct TableInOutOutput {
pub(crate) items: Vec<(RecordBatch, Option<HashMap<String, String>>)>,
}
impl TableInOutOutput {
pub fn emit(&mut self, batch: RecordBatch) {
self.items.push((batch, None));
}
pub fn emit_with(&mut self, batch: RecordBatch, opts: EmitOptions) -> Result<()> {
let mut md: HashMap<String, String> = opts.metadata.unwrap_or_default();
if let Some(parent_rows) = opts.parent_rows {
if parent_rows.len() != batch.num_rows() {
return Err(RpcError::runtime_error(format!(
"emit_with(parent_rows=...) length {} != batch.num_rows {}; parent_rows \
must carry exactly one input-row index per emitted output row",
parent_rows.len(),
batch.num_rows()
)));
}
if !parent_rows.is_empty() {
let mut raw = Vec::with_capacity(parent_rows.len() * 4);
for v in &parent_rows {
raw.extend_from_slice(&v.to_le_bytes());
}
md.insert(
PARENT_ROW_METADATA_KEY.to_string(),
crate::partition::base64_encode(&raw),
);
}
}
if let Some(cc) = opts.cache_control {
md.extend(cc.to_metadata());
}
self.items
.push((batch, if md.is_empty() { None } else { Some(md) }));
Ok(())
}
}
pub trait TableInOutFunction: Send + Sync {
fn name(&self) -> &str;
fn metadata(&self) -> FunctionMetadata;
fn argument_specs(&self) -> Vec<ArgSpec>;
fn secret_lookups(&self, _params: &BindParams) -> Vec<crate::secrets::SecretLookup> {
Vec::new()
}
fn on_bind(&self, params: &BindParams) -> Result<BindResponse> {
let input = params
.input_schema
.clone()
.ok_or_else(|| RpcError::value_error("table-in-out requires an input schema"))?;
Ok(BindResponse {
output_schema: input,
opaque_data: Vec::new(),
})
}
fn process(&self, params: &ProcessParams, batch: &RecordBatch) -> Result<Vec<RecordBatch>> {
Ok(vec![project_batch(batch, ¶ms.output_schema)?])
}
fn process_out(
&self,
params: &ProcessParams,
batch: &RecordBatch,
out: &mut TableInOutOutput,
) -> Result<()> {
for b in self.process(params, batch)? {
out.emit(b);
}
Ok(())
}
fn has_finish(&self) -> bool {
false
}
fn finish(&self, _params: &ProcessParams) -> Result<Vec<RecordBatch>> {
Ok(Vec::new())
}
}
pub fn project_batch(batch: &RecordBatch, schema: &SchemaRef) -> Result<RecordBatch> {
if batch.schema().fields() == schema.fields() {
return Ok(batch.clone());
}
let mut cols = Vec::with_capacity(schema.fields().len());
for f in schema.fields() {
match batch.schema().column_with_name(f.name()) {
Some((i, _)) => cols.push(batch.column(i).clone()),
None => {
return Err(RpcError::runtime_error(format!(
"projection column '{}' not found in input",
f.name()
)))
}
}
}
RecordBatch::try_new(schema.clone(), cols)
.map_err(|e| RpcError::runtime_error(format!("project batch: {e}")))
}
pub fn arc(s: Schema) -> Arc<Schema> {
Arc::new(s)
}