use std::collections::BTreeMap;
use arrow_array::RecordBatch;
use arrow_ipc::writer::{DictionaryTracker, IpcDataGenerator, IpcWriteContext, IpcWriteOptions};
use arrow_schema::{DataType, Schema};
use crate::batch::BatchEncode;
use crate::error::{Error, Result};
use super::conversion::{
hour_ticks, record_batch_from_columns, take_rows, timestamp_at, timestamp_column,
};
pub const SEMANTIC_TYPE_KEY: &str = "greptime:semantic_type";
pub const TYPE_EXTENSION_KEY: &str = "greptime:type";
pub const JSON_TYPE_VALUE: &str = "Json";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FlightCompression {
#[default]
None,
Lz4Frame,
Zstd,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EncodedFlightBody {
pub data_header: Vec<u8>,
pub data_body: Vec<u8>,
}
pub struct FlightIpcEncoder {
generator: IpcDataGenerator,
tracker: DictionaryTracker,
options: IpcWriteOptions,
context: IpcWriteContext,
}
impl FlightIpcEncoder {
pub fn new(compression: FlightCompression) -> Result<Self> {
let kind = match compression {
FlightCompression::None => None,
FlightCompression::Lz4Frame => Some(arrow_ipc::CompressionType::LZ4_FRAME),
FlightCompression::Zstd => Some(arrow_ipc::CompressionType::ZSTD),
};
let options = IpcWriteOptions::default()
.try_with_compression(kind)
.map_err(|error| Error::Encode(error.to_string()))?;
Ok(Self {
generator: IpcDataGenerator::default(),
tracker: DictionaryTracker::new(false),
options,
context: IpcWriteContext::default(),
})
}
pub fn encode_schema(&mut self, schema: &Schema) -> Result<EncodedFlightBody> {
reject_dictionaries(schema)?;
let encoded = self.generator.schema_to_bytes_with_dictionary_tracker(
schema,
&mut self.tracker,
&self.options,
);
Ok(EncodedFlightBody {
data_header: encoded.ipc_message,
data_body: encoded.arrow_data,
})
}
pub fn encode_batch(&mut self, batch: &RecordBatch) -> Result<EncodedFlightBody> {
reject_dictionaries(batch.schema().as_ref())?;
let (dictionaries, encoded) = self
.generator
.encode(batch, &mut self.tracker, &self.options, &mut self.context)
.map_err(|error| Error::Encode(error.to_string()))?;
if !dictionaries.is_empty() {
return Err(Error::Encode(
"dictionary columns are not supported on Arrow Flight streams".into(),
));
}
Ok(EncodedFlightBody {
data_header: encoded.ipc_message,
data_body: encoded.arrow_data,
})
}
}
pub fn batch_to_record_batch<B: BatchEncode + ?Sized>(batch: &B) -> Result<RecordBatch> {
let request = batch.encode_insert_request()?;
let rows = usize::try_from(request.row_count)
.map_err(|_| Error::SchemaViolation("row count does not fit usize".into()))?;
record_batch_from_columns(&request.columns, rows)
}
pub fn timestamp_bounds(batch: &RecordBatch) -> Result<(Option<i64>, Option<i64>)> {
if batch.num_rows() == 0 {
return Ok((None, None));
}
let (index, _) = timestamp_column(batch.schema().as_ref())?;
let column = batch.column(index);
let mut min = i64::MAX;
let mut max = i64::MIN;
for row in 0..batch.num_rows() {
let value = timestamp_at(column, row)?;
min = min.min(value);
max = max.max(value);
}
Ok((Some(min), Some(max)))
}
pub fn partition_record_batch_by_hour(batch: &RecordBatch) -> Result<Vec<RecordBatch>> {
if batch.num_rows() == 0 {
return Ok(vec![batch.clone()]);
}
let (index, unit) = timestamp_column(batch.schema().as_ref())?;
let ticks = hour_ticks(unit);
let column = batch.column(index);
let mut windows: BTreeMap<i64, Vec<usize>> = BTreeMap::new();
for row in 0..batch.num_rows() {
let value = timestamp_at(column, row)?;
let start = value
.checked_div_euclid(ticks)
.ok_or_else(|| Error::SchemaViolation("timestamp hour window overflowed".into()))?;
windows.entry(start).or_default().push(row);
}
let mut parts = Vec::with_capacity(windows.len());
for indices in windows.into_values() {
parts.push(take_rows(batch, &indices)?);
}
Ok(parts)
}
fn reject_dictionaries(schema: &Schema) -> Result<()> {
if schema
.fields()
.iter()
.any(|field| matches!(field.data_type(), DataType::Dictionary(_, _)))
{
return Err(Error::Encode(
"dictionary columns are not supported on Arrow Flight streams".into(),
));
}
Ok(())
}