- Provide a compile-time schema for Arrow using Rust types and macros.
- Generate monomorphized code per column index and base type (no runtime
DataTypeswitching). - Add a unified facade to build
RecordBatches from rows for both compile-time and runtime schemas, inferring capacity from iterator size hints. - Surface structured errors for the dynamic path (arity/type/builder/nullability errors). Nullability is validated before finishing via
try_finish_into_batchto return a descriptive error instead of panicking. Keep the typed path infallible/zero-cost by default. - Keep the unified facade focused on batch building (projection helpers are out of scope).
- Encode each column’s Arrow type and nullability at the type level via
#[derive(Record)]. - Enable compile-time dispatch across columns and base types using traits/const-generics (no
match DataTypeat runtime). - Generate typed Arrow builders/arrays for each column from the compile-time schema.
- Provide a unified facade to build
RecordBatches from rows for both typed and dynamic schemas with inferred capacity. - Surface errors in the dynamic path (row arity, type mismatches, builder failures, nullability violations) via a structured error type;
try_finish_into_batchvalidates nullability for columns/fields/items and returns an error with path context. - Keep ergonomics close to idiomatic Rust: field attributes express Arrow specifics; Option/Nullability is explicit.
- Keep projection helpers out of the unified facade to maintain focus and minimal surface area.
-
#[derive(Record)]on a plain Rust struct generates:impl Record for Twithconst LEN: usize.- Per-column type metadata via
ColAt<I>exposingtype Rust,type ColumnBuilder,type ColumnArray,const NULLABLE,const NAME, andfn data_type() -> DataType. - A type-directed “for-each column” expansion hook via
ForEachCol::for_each_col.
-
Implemented traits (current):
Record,ColAt<I>,ForEachCol.- Row-based building:
BuildRowswith generated<Type>Buildersand<Type>Arrays. - Nested struct support:
StructMeta(child fields + struct builder) andAppendStruct(append children into a StructBuilder).
-
Nullability:
- Mode A: Use
Option<T>in the field type. - Mode B:
#[arrow(nullable)]attribute even if field type is non-Option(explicit override). Derived code maps toNULLABLEconst.
- Mode A: Use
-
Type mapping trait:
trait ArrowBinding { type Builder; type Array: arrow_array::Array; fn data_type() -> DataType; ... }- Implementations map Rust types directly to arrow-rs typed builders/arrays without runtime dispatch.
-
Builders API (row-based):
BuildRowsderive emits<Type>Buildersand<Type>Arrays.<Type>Buildersmethods:append_row(row),append_rows(iter),append_null_row(),append_option_row(Option<row>),append_option_rows(iter);finish()returns<Type>Arrays.
-
Nested structs are the default for struct-typed fields (no attribute needed), powered by
AppendStructandStructMeta.- Future: optional
into_record_batch()bridge when needed.
- Future: optional
-
Runtime schema:
schema::<R>()available fromSchemaMeta(typed). For runtime-only schemas, usetyped-arrow-dyn::DynSchema.
-
Compile-Time Dispatch (No runtime matches)
-
Column visitor/iterator (generated by derive):
trait ColumnVisitor { fn visit<const I: usize, R>(FieldMeta<R>); }for_each_col::<V>()expands toV::visit::<I, Rust>(...)for each column at compile time.
-
Kernel pattern: implement
ColumnVisitorfor an operation; the monomorphized instances per column/base-type fall out of generics. -
Macro & Attribute Design
-
#[derive(Record)]with field-level attributes:- Struct-typed fields are nested
Structcolumns by default; useOption<Nested>on the field for column nullability.
- Struct-typed fields are nested
-
#[derive(Union)](enums) supports Dense/Sparse modes and attributes:- Container:
#[union(mode = "dense"|"sparse", null_variant = "Var", tags(A=10, B=7))] - Variant:
#[union(tag = 42)],#[union(field = "name")],#[union(null)]
- Container:
-
Map uses wrappers rather than attributes:
Map<K, V, const SORTED: bool>andOrderedMap<K, V>; value-nullability viaOption<V>. -
Helper macro for column iteration (optional if derive emits trait impl):
for_each_col!(RecordType, |I, Meta| { /* compile-time expanded body */ });
- Default mapping for well-known Rust types:
i8/i16/i32/i64,u8/u16/u32/u64,f32/f64,boolmap to Arrow primitives.String→Utf8,Vec<u8>→Binary.Option<T>toggles nullability at the column level.
- Nested & specialized wrappers (current):
- Lists:
List<T>(items non-null),List<Option<T>>(items nullable); LargeList and FixedSizeList variants.- Dictionary:
Dictionary<K, V>for Utf8/Binary/FixedSizeBinary/primitives. - Timestamp:
Timestamp<U>(unit markers) andTimestampTz<U, Z>(timezone markers, e.g.,Utc). - Decimal:
Decimal128<const P: u8, const S: i8>,Decimal256<const P: u8, const S: i8>. - Map:
Map<K, V, const SORTED: bool>; nullable values viaOption<V>. - Ordered Map:
OrderedMap<K, V>with sorted keys (keys_sorted = true). - Union:
#[derive(Union)]with Dense and Sparse modes.
- Dictionary:
- Row/cell types:
DynRow(Vec<Option<DynCell>>)holds per-column optional values.DynCellvariants cover Arrow logical types (bool, ints, floats, utf8/binary, dictionary via value types, and nested Struct/List/LargeList/FixedSizeList).
- Builders and schema:
DynSchema(Arc<Schema>)andDynBuilderscreate and finish batches for runtime schemas.DynRow::append_intovalidates arity and value compatibility before appending.
- Nullability enforcement: dynamic builders do not check column/field/item nullability during appends; a validator runs at
try_finish_into_batchto catch violations and return a structured error with the offending path and index.- Factory:
new_dyn_builder(dt: &DataType)selects a concrete builder implementation; nullability is not passed to the factory and is enforced by Arrow.
- Factory:
- Errors:
DynErrorvariants:ArityMismatch,TypeMismatch { col, expected },Builder { message },Append { col, message }.- All dynamic append operations return
Resultand propagate context; no silent mismatches.
use typed_arrow::prelude::*;
#[derive(Record)]
struct Address { city: String, zip: Option<i32> }
#[derive(Record)]
struct Person {
id: i64,
// Nested struct field (no attribute needed)
address: Option<Address>,
email: Option<String>,
}
fn build_arrays_from_rows(rows: Vec<Option<Person>>) {
let mut b = <Person as BuildRows>::new_builders(rows.len());
b.append_option_rows(rows);
let arrays = b.finish();
// arrays.id: PrimitiveArray<Int64Type>
// arrays.address: StructArray
// arrays.email: StringArray
}
// Example compile-time dispatch
struct Count;
impl ColumnVisitor for Count {
fn visit<const I: usize, R>(_m: FieldMeta<R>) {
let _ = I;
}
}
fn debug_schema<T: ForEachCol>() { T::for_each_col::<Count>(); }Typed schema:
use typed_arrow::prelude::*;
use typed_arrow_unified::{SchemaLike, Typed};
#[derive(Record)]
struct Person { id: i64, name: Option<String> }
fn build_batch_typed() -> arrow_array::RecordBatch {
let rows = vec![
Person { id: 1, name: Some("a".into()) },
Person { id: 2, name: None },
];
let schema = Typed::<Person>::default();
schema.build_batch(rows).unwrap()
}Dynamic schema:
use arrow_schema::{DataType, Field, Schema};
use typed_arrow_dyn::{DynCell, DynRow, DynSchema};
use typed_arrow_unified::SchemaLike;
fn build_batch_dynamic() -> arrow_array::RecordBatch {
let schema = Schema::new(vec![
Field::new("id", DataType::Int64, false),
Field::new("name", DataType::Utf8, true),
]);
let dyn_schema = DynSchema::new(schema);
let rows = vec![
DynRow(vec![Some(DynCell::I64(1)), Some(DynCell::Str("a".into()))]),
DynRow(vec![Some(DynCell::I64(2)), None]),
];
dyn_schema.build_batch(rows).expect("valid rows")
}- Timezones at type level: use const
&'static strgeneric or a closed set of TZ types? Start with UTC only. - Decimal overflow/scale enforcement in builders: enforce at append-time or constructor?
- Dictionary-encoded columns: how to expose keys/types at compile-time without specialization.
- Stable specialization: avoid it; prefer marker traits and explicit impls.
- Arrow-rs compatibility: gate features for versioned differences in array/builder APIs.
- Rust 1.75+ (const generics for integers are sufficient; advance if we adopt additional const parameters).
src/— core library:schema.rs:Record,ColAt<I>, visitors, compile-time metadata.bridge/— Arrow interop:mod.rs:ArrowBindingtrait and public re-exportsprimitives.rs: numeric primitives,bool,f16strings.rs:String(Utf8),LargeUtf8binary.rs:Vec<u8>(Binary),[u8; N](FixedSizeBinary),LargeBinarydecimals.rs:Decimal128,Decimal256lists.rs:List,LargeList,FixedSizeListmap.rs:Map,OrderedMapdictionary.rs:Dictionary,DictKeyand value implstemporal.rs:Date32/64,Time32/64,Duration,Timestamp,TimestampTz, TZ markersintervals.rs:IntervalYearMonth,IntervalDayTime,IntervalMonthDayNanorecord_struct.rs: blanket impl forT: Record + StructMeta→StructArraycolumn.rs:data_type_of<R, I>(),ColumnBuilder<R, I>
lib.rs: crate entry, prelude exports.
typed-arrow-derive/— proc-macro crate implementing#[derive(Record)]and#[derive(Union)].tests/— integration tests (e.g.,primitive_macro.rs).docs/— design notes (e.g.,nested-types.md).examples/— runnable demos (e.g.,examples/11_map.rs).
cargo build— builds the workspace (library + proc-macro).cargo test— runs unit and integration tests.cargo check— fast type-check of the workspace.- Optional:
RUSTFLAGS='-D warnings' cargo buildto treat warnings as errors.
- Rust 2021 edition; keep code idiomatic and minimal.
- Run
rustfmt(standard Rust formatting). Useclippylocally for hints. - Naming: modules
snake_case, types and traitsCamelCase, functionssnake_case, constantsSCREAMING_SNAKE_CASE. - Public API docs: crates use
#![deny(missing_docs)]— document new public items across typed, dynamic, and unified crates.
- Prefer focused tests; add integration tests in
tests/for end‑to‑end flows. - Validate that
ColAt<I>exposesfn data_type(),ColumnBuilder,ColumnArrayand that builders produce typed arrays. - Exercise row-based building (
append_row,append_option_rows) and nested struct append. - Validate DataType shapes (child names,
keys_sorted, union tags/fields) and append semantics. - Prefer names/tags over child indices when asserting nested/union children.
- Run locally with
cargo test -qandcargo clippy --workspace -D warnings.
- Commits: clear, imperative mood (e.g., “Add ArrowBinding for u32”).
- Prefer small, focused commits with meaningful scope; reference issues when relevant.
- PRs must include:
- What changed and why (problem statement + approach).
- Tests or rationale for test coverage.
- Notes on API or behavior changes (breaking/experimental).
-
Compile-time schema via
#[derive(Record)]generatesRecord,ColAt<I>, andForEachCol. -
ColAt<I>exposesRust(inner type),data_type(),ColumnBuilder, andColumnArray— no runtimeDataTypeswitching. -
bridge::ArrowBindingmaps Rust types and wrappers (e.g., primitives, Utf8/Binary, List/LargeList/FixedSizeList, Map/OrderedMap, Decimal128/256, Timestamp/TimestampTz, Dictionary, and Union via derive) to Arrow builders/arrays. -
Unified facade:
SchemaLike::build_batchunifies batch assembly for typed (Typed<R>) and dynamic (DynSchema/Arc<Schema>).BuildersLikeabstracts append/finish withResult-returning dynamic path and infallible typed path.