timeseries-table-format 0.5.0

Append-only time-series table format with gap/overlap tracking
Documentation
use std::{
    path::{Path, PathBuf},
    sync::Arc,
};

use arrow::array::{
    BinaryBuilder, BooleanBuilder, Float64Builder, Int64Builder, StringBuilder,
    TimestampMillisecondBuilder,
};
use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
use arrow::record_batch::RecordBatch;
use parquet::arrow::ArrowWriter;

type TestResult<T = ()> = Result<T, Box<dyn std::error::Error>>;

pub fn write_parquet_rows(path: &Path, rows: usize) -> TestResult {
    write_parquet_rows_with_base(path, rows, 1_700_000_000_000i64)
}

pub fn write_parquet_rows_with_base(path: &Path, rows: usize, base_ts: i64) -> TestResult {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    let mut ts_builder = TimestampMillisecondBuilder::with_capacity(rows);
    let mut sym_builder = StringBuilder::new();
    let mut price_builder = Float64Builder::with_capacity(rows);
    let mut volume_builder = Int64Builder::with_capacity(rows);
    let mut trade_builder = BooleanBuilder::with_capacity(rows);
    let mut venue_builder = StringBuilder::new();
    let mut payload_builder = BinaryBuilder::new();

    let mut seed = 0xBAD_5EED_u64;
    for i in 0..rows {
        seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
        let rnd = seed;
        let ts = base_ts + (i as i64) * 1_000;
        let symbol = format!("SYM{i}");
        let price = 100.0 + (rnd % 10_000) as f64 / 100.0;
        let volume = 1_000 + (rnd % 5_000) as i64;
        let is_trade = i % 2 == 0;
        let venue = if i % 5 == 0 {
            None
        } else {
            Some(format!("X{}", (rnd % 7) + 1))
        };
        let payload = vec![i as u8, (i.wrapping_mul(3)) as u8];

        ts_builder.append_value(ts);
        sym_builder.append_value(symbol);
        price_builder.append_value(price);
        volume_builder.append_value(volume);
        trade_builder.append_value(is_trade);
        match venue {
            Some(v) => venue_builder.append_value(v),
            None => venue_builder.append_null(),
        }
        payload_builder.append_value(payload);
    }

    let schema = Schema::new(vec![
        Field::new(
            "ts",
            DataType::Timestamp(TimeUnit::Millisecond, None),
            false,
        ),
        Field::new("symbol", DataType::Utf8, false),
        Field::new("price", DataType::Float64, false),
        Field::new("volume", DataType::Int64, false),
        Field::new("is_trade", DataType::Boolean, false),
        Field::new("venue", DataType::Utf8, true),
        Field::new("payload", DataType::Binary, false),
    ]);

    let batch = RecordBatch::try_new(
        Arc::new(schema.clone()),
        vec![
            Arc::new(ts_builder.finish()) as _,
            Arc::new(sym_builder.finish()),
            Arc::new(price_builder.finish()),
            Arc::new(volume_builder.finish()),
            Arc::new(trade_builder.finish()),
            Arc::new(venue_builder.finish()),
            Arc::new(payload_builder.finish()),
        ],
    )?;

    let file = std::fs::File::create(path)?;
    let props = parquet::file::properties::WriterProperties::builder().build();
    let mut writer = ArrowWriter::try_new(file, Arc::new(schema), Some(props))?;
    writer.write(&batch)?;
    writer.close()?;

    Ok(())
}

#[allow(dead_code)]
pub fn table_root(tmp: &tempfile::TempDir, name: &str) -> PathBuf {
    tmp.path().join(name)
}