use crate::{Error, Result};
use arrow::record_batch::RecordBatch;
#[cfg(feature = "parquet-io")]
use std::path::Path;
pub const MORSEL_SIZE_BYTES: usize = 128 * 1024 * 1024;
#[cfg(feature = "tokio")]
const MAX_IN_FLIGHT_TRANSFERS: usize = 2;
pub struct StorageEngine {
batches: Vec<RecordBatch>,
}
impl StorageEngine {
#[must_use]
pub const fn new(batches: Vec<RecordBatch>) -> Self {
Self { batches }
}
#[cfg(feature = "parquet-io")]
pub fn load_parquet<P: AsRef<Path>>(path: P) -> Result<Self> {
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use std::fs::File;
let file = File::open(path.as_ref())
.map_err(|e| Error::StorageError(format!("Failed to open Parquet file: {e}")))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| Error::StorageError(format!("Failed to parse Parquet file: {e}")))?;
let reader = builder
.build()
.map_err(|e| Error::StorageError(format!("Failed to create Parquet reader: {e}")))?;
let mut batches = Vec::new();
for batch in reader {
let batch = batch
.map_err(|e| Error::StorageError(format!("Failed to read record batch: {e}")))?;
batches.push(batch);
}
Ok(Self { batches })
}
#[must_use]
pub fn batches(&self) -> &[RecordBatch] {
&self.batches
}
#[must_use]
pub fn morsels(&self) -> MorselIterator<'_> {
MorselIterator::new(&self.batches)
}
pub fn append_batch(&mut self, batch: RecordBatch) -> Result<()> {
if !self.batches.is_empty() {
let existing_schema = self.batches[0].schema();
if batch.schema() != existing_schema {
return Err(Error::StorageError(format!(
"Schema mismatch: expected {:?}, got {:?}",
existing_schema,
batch.schema()
)));
}
}
self.batches.push(batch);
Ok(())
}
#[deprecated(
since = "0.1.0",
note = "Trueno-DB is OLAP-only. Use append_batch() for bulk data loads."
)]
#[allow(clippy::unused_self)]
pub fn update_row(&mut self, _row_id: usize, _values: RecordBatch) -> Result<()> {
Err(Error::StorageError(
"Single-row updates not supported in columnar storage. \
Use append_batch() for bulk re-analysis instead."
.to_string(),
))
}
}
pub struct MorselIterator<'a> {
batches: &'a [RecordBatch],
current_batch_idx: usize,
current_offset: usize,
morsel_rows: usize,
}
impl<'a> MorselIterator<'a> {
fn new(batches: &'a [RecordBatch]) -> Self {
let morsel_rows = batches.first().map_or(0, Self::calculate_morsel_rows);
Self { batches, current_batch_idx: 0, current_offset: 0, morsel_rows }
}
fn calculate_morsel_rows(batch: &RecordBatch) -> usize {
let num_rows = batch.num_rows();
if num_rows == 0 {
return 0;
}
let total_bytes = batch.get_array_memory_size();
let bytes_per_row = total_bytes / num_rows;
if bytes_per_row == 0 {
return num_rows; }
MORSEL_SIZE_BYTES / bytes_per_row
}
}
impl Iterator for MorselIterator<'_> {
type Item = RecordBatch;
fn next(&mut self) -> Option<Self::Item> {
if self.current_batch_idx >= self.batches.len() {
return None;
}
let current_batch = &self.batches[self.current_batch_idx];
if self.current_offset >= current_batch.num_rows() {
self.current_batch_idx += 1;
self.current_offset = 0;
return self.next(); }
let remaining_rows = current_batch.num_rows() - self.current_offset;
let slice_length = remaining_rows.min(self.morsel_rows);
let morsel = current_batch.slice(self.current_offset, slice_length);
self.current_offset += slice_length;
Some(morsel)
}
}
#[cfg(feature = "tokio")]
pub struct GpuTransferQueue {
sender: tokio::sync::mpsc::Sender<RecordBatch>,
receiver: tokio::sync::mpsc::Receiver<RecordBatch>,
}
#[cfg(feature = "tokio")]
impl GpuTransferQueue {
#[must_use]
pub fn new() -> Self {
let (sender, receiver) = tokio::sync::mpsc::channel(MAX_IN_FLIGHT_TRANSFERS);
Self { sender, receiver }
}
pub async fn enqueue(&self, batch: RecordBatch) -> Result<()> {
self.sender.send(batch).await.map_err(|_| Error::QueueClosed)
}
pub async fn dequeue(&mut self) -> Option<RecordBatch> {
self.receiver.recv().await
}
#[must_use]
pub fn sender(&self) -> tokio::sync::mpsc::Sender<RecordBatch> {
self.sender.clone()
}
}
#[cfg(feature = "tokio")]
impl Default for GpuTransferQueue {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{Float32Array, Int32Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use std::sync::Arc;
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_possible_wrap)]
#[allow(clippy::cast_precision_loss)]
fn create_test_batch(num_rows: usize) -> RecordBatch {
let schema = Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("value", DataType::Float32, false),
Field::new("name", DataType::Utf8, false),
]);
let id_array = Int32Array::from_iter_values(0..num_rows as i32);
let value_array = Float32Array::from_iter_values((0..num_rows).map(|i| i as f32));
let name_array = StringArray::from_iter_values((0..num_rows).map(|i| format!("name_{i}")));
RecordBatch::try_new(
Arc::new(schema),
vec![Arc::new(id_array), Arc::new(value_array), Arc::new(name_array)],
)
.unwrap()
}
#[test]
fn test_morsel_iterator_splits_correctly() {
let batch = create_test_batch(1000);
let batches = vec![batch];
let iter = MorselIterator::new(&batches);
let morsels: Vec<_> = iter.collect();
let total_rows: usize = morsels.iter().map(RecordBatch::num_rows).sum();
assert_eq!(total_rows, 1000);
for morsel in &morsels {
assert!(morsel.get_array_memory_size() <= MORSEL_SIZE_BYTES);
}
}
#[test]
fn test_morsel_iterator_empty_batch() {
let batch = create_test_batch(0);
let batches = vec![batch];
let iter = MorselIterator::new(&batches);
assert_eq!(iter.count(), 0);
}
#[test]
fn test_append_batch_olap_pattern() {
let mut storage = StorageEngine::new(vec![]);
let batch1 = create_test_batch(100);
let batch2 = create_test_batch(200);
storage.append_batch(batch1).unwrap();
storage.append_batch(batch2).unwrap();
assert_eq!(storage.batches().len(), 2);
assert_eq!(storage.batches()[0].num_rows(), 100);
assert_eq!(storage.batches()[1].num_rows(), 200);
}
#[test]
fn test_append_batch_schema_validation() {
let mut storage = StorageEngine::new(vec![]);
let batch1 = create_test_batch(100);
storage.append_batch(batch1).unwrap();
let incompatible_schema =
Schema::new(vec![Field::new("different_field", DataType::Int32, false)]);
let incompatible_batch = RecordBatch::try_new(
Arc::new(incompatible_schema),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
)
.unwrap();
let result = storage.append_batch(incompatible_batch);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Schema mismatch"));
}
#[test]
#[allow(deprecated)]
fn test_update_row_fails_oltp_pattern() {
let mut storage = StorageEngine::new(vec![]);
let batch = create_test_batch(100);
let result = storage.update_row(0, batch);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Single-row updates not supported"));
}
#[test]
fn test_morsel_iterator_multiple_batches() {
let batch1 = create_test_batch(500);
let batch2 = create_test_batch(500);
let batches = vec![batch1, batch2];
let iter = MorselIterator::new(&batches);
let morsels: Vec<_> = iter.collect();
let total_rows: usize = morsels.iter().map(RecordBatch::num_rows).sum();
assert_eq!(total_rows, 1000);
}
#[tokio::test]
async fn test_gpu_transfer_queue_basic() {
let mut queue = GpuTransferQueue::new();
let batch = create_test_batch(100);
queue.enqueue(batch.clone()).await.unwrap();
let received = queue.dequeue().await.unwrap();
assert_eq!(received.num_rows(), 100);
}
#[tokio::test]
async fn test_gpu_transfer_queue_bounded() {
use tokio::time::{timeout, Duration};
let queue = GpuTransferQueue::new();
let batch = create_test_batch(100);
queue.enqueue(batch.clone()).await.unwrap();
queue.enqueue(batch.clone()).await.unwrap();
let result = timeout(Duration::from_millis(100), queue.enqueue(batch)).await;
assert!(result.is_err(), "Queue should be full and block");
}
#[tokio::test]
async fn test_gpu_transfer_queue_concurrent_enqueue_dequeue() {
use tokio::task;
let mut queue = GpuTransferQueue::new();
let sender = queue.sender();
let enqueue_handle = task::spawn(async move {
for i in 0..5 {
let batch = create_test_batch(100 * (i + 1));
sender.send(batch).await.unwrap();
}
});
for i in 0..5 {
let batch = queue.dequeue().await.unwrap();
assert_eq!(batch.num_rows(), 100 * (i + 1));
}
enqueue_handle.await.unwrap();
}
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_morsel_iterator_preserves_all_rows(
num_rows in 1usize..100_000
) {
let batch = create_test_batch(num_rows);
let original_rows = batch.num_rows();
let batches = vec![batch];
let iter = MorselIterator::new(&batches);
let total_morsel_rows: usize = iter.map(|m| m.num_rows()).sum();
prop_assert_eq!(original_rows, total_morsel_rows);
}
#[test]
fn prop_morsel_size_within_limit(
num_rows in 1usize..100_000
) {
let batch = create_test_batch(num_rows);
let batches = vec![batch];
let iter = MorselIterator::new(&batches);
for morsel in iter {
let size = morsel.get_array_memory_size();
prop_assert!(
size <= MORSEL_SIZE_BYTES,
"Morsel size {} exceeds limit {}",
size,
MORSEL_SIZE_BYTES
);
}
}
#[test]
fn prop_multiple_batches_preserve_rows(
batch_sizes in prop::collection::vec(1usize..10_000, 1..10)
) {
let total_expected: usize = batch_sizes.iter().sum();
let batches: Vec<_> = batch_sizes.iter()
.map(|&size| create_test_batch(size))
.collect();
let iter = MorselIterator::new(&batches);
let total_actual: usize = iter.map(|m| m.num_rows()).sum();
prop_assert_eq!(total_expected, total_actual);
}
#[test]
fn prop_empty_batches_handled(
num_empty in 0usize..10
) {
let batches: Vec<_> = (0..num_empty)
.map(|_| create_test_batch(0))
.collect();
let iter = MorselIterator::new(&batches);
let total_rows: usize = iter.map(|m| m.num_rows()).sum();
prop_assert_eq!(0, total_rows);
}
}
}
}