use std::io::Cursor;
use arrow_array::{Array, BinaryArray, RecordBatch};
use arrow_schema::{Schema, SchemaRef};
use crate::errors::{Result, RpcError};
use crate::log::LogLevel;
use crate::metadata::{
LOG_LEVEL_KEY, PROTOCOL_VERSION_KEY, REQUEST_ID_KEY, REQUEST_VERSION, REQUEST_VERSION_KEY,
RPC_METHOD_KEY, SERVER_ID_KEY, STATE_KEY,
};
use crate::server::Request;
use crate::wire::{empty_batch, md_get, Metadata, StreamReader, StreamWriter};
pub fn read_request(data: &[u8]) -> Result<Request> {
let mut reader = StreamReader::new(Cursor::new(data))?;
let (batch, metadata) = reader
.read_next()?
.ok_or_else(|| RpcError::protocol_error("empty IPC stream"))?;
reader.drain()?;
Request::from_read_batch(batch, metadata, true)
}
pub fn write_request(
method: &str,
params: &RecordBatch,
protocol_version: Option<&str>,
request_id: Option<&str>,
) -> Result<Vec<u8>> {
let mut md = Metadata::new();
md.insert(RPC_METHOD_KEY.to_string(), method.to_string());
md.insert(REQUEST_VERSION_KEY.to_string(), REQUEST_VERSION.to_string());
if let Some(id) = request_id.filter(|s| !s.is_empty()) {
md.insert(REQUEST_ID_KEY.to_string(), id.to_string());
}
if let Some(pv) = protocol_version.filter(|s| !s.is_empty()) {
md.insert(PROTOCOL_VERSION_KEY.to_string(), pv.to_string());
}
let mut buf = Vec::new();
{
let mut sw = StreamWriter::new(&mut buf, params.schema().as_ref())?;
sw.write(params, Some(&md))?;
sw.finish()?;
}
Ok(buf)
}
pub fn build_error_stream(
err: &RpcError,
schema: Option<&Schema>,
server_id: Option<&str>,
) -> Result<Vec<u8>> {
let empty = Schema::empty();
let schema = schema.unwrap_or(&empty);
let mut buf = Vec::new();
{
let mut sw = StreamWriter::new(&mut buf, schema)?;
let mut md = crate::server::build_error_metadata(err, server_id.unwrap_or(""), "");
if server_id.is_none_or(str::is_empty) {
md.remove(SERVER_ID_KEY);
}
sw.write(&empty_batch(schema)?, Some(&md))?;
sw.finish()?;
}
Ok(buf)
}
fn scan_batch_metadata<T>(data: &[u8], mut f: impl FnMut(&Metadata) -> Option<T>) -> Option<T> {
let mut offset = 0usize;
while offset < data.len() {
let start = offset;
let mut cursor = Cursor::new(&data[offset..]);
{
let Ok(mut reader) = StreamReader::new(&mut cursor) else {
return None;
};
while let Ok(Some((_batch, md))) = reader.read_next() {
if let Some(found) = f(&md) {
return Some(found);
}
}
}
offset += cursor.position() as usize;
if offset == start {
return None; }
}
None
}
pub fn find_state_token(data: &[u8]) -> Option<String> {
scan_batch_metadata(data, |md| {
md_get(md, STATE_KEY)
.filter(|t| !t.is_empty())
.map(str::to_string)
})
}
pub fn find_protocol_version(data: &[u8]) -> Option<String> {
scan_batch_metadata(data, |md| {
md_get(md, PROTOCOL_VERSION_KEY)
.filter(|v| !v.is_empty())
.map(str::to_string)
})
}
pub fn read_unary_result(data: &[u8]) -> Result<Option<(SchemaRef, Vec<u8>)>> {
let mut reader = StreamReader::new(Cursor::new(data))?;
while let Some((batch, md)) = reader.read_next()? {
if batch.num_rows() > 0 {
let Ok(idx) = batch.schema().index_of("result") else {
return Ok(None);
};
let Some(col) = batch.column(idx).as_any().downcast_ref::<BinaryArray>() else {
return Ok(None);
};
if col.is_null(0) {
return Ok(None);
}
return Ok(Some((batch.schema(), col.value(0).to_vec())));
}
match md_get(&md, LOG_LEVEL_KEY) {
Some(level) if level != LogLevel::Exception.as_str() => continue,
_ => return Ok(None),
}
}
Ok(None)
}
pub fn write_unary_result(envelope_schema: &Schema, result_bytes: &[u8]) -> Result<Vec<u8>> {
let col = BinaryArray::from(vec![Some(result_bytes)]);
let batch = RecordBatch::try_new(
SchemaRef::from(envelope_schema.clone()),
vec![std::sync::Arc::new(col)],
)
.map_err(|e| RpcError::runtime_error(format!("unary result batch: {e}")))?;
let mut buf = Vec::new();
{
let mut sw = StreamWriter::new(&mut buf, envelope_schema)?;
sw.write(&batch, None)?;
sw.finish()?;
}
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::Int64Array;
use arrow_schema::{DataType, Field};
use std::sync::Arc;
fn params_batch(n: i64) -> RecordBatch {
let schema = Arc::new(Schema::new(vec![Field::new("n", DataType::Int64, true)]));
RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(vec![n]))]).unwrap()
}
fn envelope_schema() -> Schema {
Schema::new(vec![Field::new("result", DataType::Binary, true)])
}
#[test]
fn request_round_trips_through_write_and_read() {
let body = write_request("bind", ¶ms_batch(7), Some("1.2.3"), Some("req-1")).unwrap();
let req = read_request(&body).unwrap();
assert_eq!(req.method, "bind");
assert_eq!(req.request_id, "req-1");
let col = req.column("n").unwrap();
let ints = col.as_any().downcast_ref::<Int64Array>().unwrap();
assert_eq!(ints.value(0), 7);
assert_eq!(find_protocol_version(&body).as_deref(), Some("1.2.3"));
}
#[test]
fn write_request_without_protocol_version_omits_the_key() {
let body = write_request("bind", ¶ms_batch(1), None, None).unwrap();
assert_eq!(find_protocol_version(&body), None);
assert_eq!(read_request(&body).unwrap().request_id, "");
}
#[test]
fn read_request_rejects_a_non_request_stream() {
let err = build_error_stream(&RpcError::runtime_error("boom"), None, None).unwrap();
assert!(read_request(&err).is_err());
}
#[test]
fn error_stream_decodes_back_to_the_error() {
let body = build_error_stream(
&RpcError::runtime_error("boom"),
Some(&envelope_schema()),
Some("srv-1"),
)
.unwrap();
let mut reader = StreamReader::new(Cursor::new(&body[..])).unwrap();
let (batch, md) = reader.read_next().unwrap().unwrap();
assert_eq!(batch.num_rows(), 0);
assert_eq!(md_get(&md, LOG_LEVEL_KEY), Some("EXCEPTION"));
assert_eq!(md_get(&md, SERVER_ID_KEY), Some("srv-1"));
assert!(read_unary_result(&body).unwrap().is_none());
}
#[test]
fn unary_result_round_trips() {
let schema = envelope_schema();
let body = write_unary_result(&schema, b"payload").unwrap();
let (got_schema, bytes) = read_unary_result(&body).unwrap().unwrap();
assert_eq!(got_schema.field(0).name(), "result");
assert_eq!(bytes, b"payload");
}
#[test]
fn read_unary_result_skips_leading_log_batches() {
let schema = envelope_schema();
let mut buf = Vec::new();
{
let mut sw = StreamWriter::new(&mut buf, &schema).unwrap();
let mut log_md = Metadata::new();
log_md.insert(LOG_LEVEL_KEY.to_string(), "INFO".to_string());
sw.write(&empty_batch(&schema).unwrap(), Some(&log_md))
.unwrap();
let batch = RecordBatch::try_new(
SchemaRef::from(schema.clone()),
vec![Arc::new(BinaryArray::from(vec![Some(&b"payload"[..])]))],
)
.unwrap();
sw.write(&batch, None).unwrap();
sw.finish().unwrap();
}
let (_, bytes) = read_unary_result(&buf).unwrap().unwrap();
assert_eq!(bytes, b"payload");
}
#[test]
fn read_unary_result_stops_at_an_error_envelope_before_a_data_batch() {
let schema = envelope_schema();
let mut buf = Vec::new();
{
let mut sw = StreamWriter::new(&mut buf, &schema).unwrap();
let err_md =
crate::server::build_error_metadata(&RpcError::runtime_error("boom"), "srv", "req");
sw.write(&empty_batch(&schema).unwrap(), Some(&err_md))
.unwrap();
let batch = RecordBatch::try_new(
SchemaRef::from(schema.clone()),
vec![Arc::new(BinaryArray::from(vec![Some(&b"payload"[..])]))],
)
.unwrap();
sw.write(&batch, None).unwrap();
sw.finish().unwrap();
}
assert!(
read_unary_result(&buf).unwrap().is_none(),
"an EXCEPTION batch must not be skipped like an INFO log batch"
);
}
#[test]
fn find_state_token_reads_the_batch_metadata() {
let batch = params_batch(1);
let mut buf = Vec::new();
{
let mut sw = StreamWriter::new(&mut buf, batch.schema().as_ref()).unwrap();
let mut md = Metadata::new();
md.insert(STATE_KEY.to_string(), "dG9rZW4=".to_string());
sw.write(&batch, Some(&md)).unwrap();
sw.finish().unwrap();
}
assert_eq!(find_state_token(&buf).as_deref(), Some("dG9rZW4="));
}
#[test]
fn find_state_token_walks_concatenated_streams() {
let header = write_request("init", ¶ms_batch(0), None, None).unwrap();
let batch = params_batch(2);
let mut data = Vec::new();
{
let mut sw = StreamWriter::new(&mut data, batch.schema().as_ref()).unwrap();
let mut md = Metadata::new();
md.insert(STATE_KEY.to_string(), "c3RhdGU=".to_string());
sw.write(&batch, Some(&md)).unwrap();
sw.finish().unwrap();
}
let body = [header, data].concat();
assert_eq!(find_state_token(&body).as_deref(), Some("c3RhdGU="));
}
#[test]
fn finders_return_none_on_garbage() {
assert_eq!(find_state_token(b"not an ipc stream"), None);
assert_eq!(find_protocol_version(b"not an ipc stream"), None);
assert_eq!(find_state_token(&[]), None);
}
#[test]
fn find_state_token_is_none_when_absent() {
let body = write_request("bind", ¶ms_batch(1), None, None).unwrap();
assert_eq!(find_state_token(&body), None);
}
}