Storage and export¶
This page covers crates/rypipe-core/src/columnar.rs:34 StrColumn, ColumnBuilder, dictionary, and crates/rypipe-core/src/arrow_export.rs plus the finish path in engine.rs:289.
Arrow types produced¶
ColumnBuilder variant |
Arrow DataType |
Array type |
|---|---|---|
String(StrColumn) |
Utf8 |
StringArray (OffsetBuffer plus Buffer plus NullBuffer) |
Int64(Vec<Option<i64>>) |
Int64 |
Int64Array |
Float64(Vec<Option<f64>>) |
Float64 |
Float64Array |
Boolean(Vec<Option<bool>>) |
Boolean |
BooleanArray |
Date32(Vec<Option<i32>>) |
Date32 |
Date32Array |
Timestamp(TimeUnit, Vec<Option<i64>>) |
Timestamp(unit, None) |
PrimitiveArray<TimestampSecondType> etc. |
Dictionary { codes, dict, index } |
Dictionary(Int32, Utf8) |
DictionaryArray<Int32Type> with Int32Array keys and StringArray values |
arrow_datatype(&self) -> DataType and to_arrow_array(&self) -> Result<ArrayRef> implement the mapping. Timestamp branches on TimeUnit to the correct PrimitiveArray type.
Null handling¶
-
StrColumnhasvalidity: Vec<bool>.to_arrowbuildsNullBufferonly if some validity is false; otherwiseNone(all valid). Offsets still advance by 0 for null entries sooffsets[i]==offsets[i+1]. -
Numeric and boolean builders are
Vec<Option<T>>.collect::<Int64Array>()etc. preserves nulls. -
Missing columns in
engines_to_record_batchesbecomenull_array(&types[name], e.row_count)(aNullArrayof the unified type). -
Value::Nulland unparseable strings both becomeNone.
String arena¶
StrColumn::push(Option<&str>) appends bytes to data and data.len() to offsets. pop truncates data to offsets.last(). append merges another column with base shifted offsets. get slices data[offsets[i]..offsets[i+1]] and does from_utf8 (safe because input was validated via simdutf8).
Capacity: with_capacity(cap) reserves cap+1 offsets and cap*16 data bytes. Pipeline passes cap = bytes.len() / 512 (min 64) or estimated_rows.
Numeric and temporal parsing¶
push and push_str for typed builders use:
lexical::parse::<i64,_>and<f64,_>forInt64/Float64s.parse::<bool>()forBooleanparse_date32(chrono::NaiveDate::parse_from_str("%Y-%m-%d")minus epoch) forDate32parse_timestamp(tries"%Y-%m-%dT%H:%M:%S%.f", then" %H:%M:%S%.f", then bare date as midnight, then converts viaand_utcto seconds, millis, micros, or nanos depending onTimeUnit) forTimestamp
Value::Int64 into Float64 widens, into String stringifies, into Dictionary encodes. Other cross type cases become None.
Dictionary¶
Dictionary { codes: Vec<Option<i32>>, dict: Vec<String>, index: HashMap<String,i32> }
-
dict_codedoesif let Some(&code) = index.get(v) { return code }elsedict.push,index.insert. -
extend_ownedforDictionaryremaps the right dictionary into the left in one pass viaremap: Vec<i32> = b_dict.iter().map(|val| dict_code(a_dict, a_index, val)).collect()then translatesa_codes.extend(b_codes.iter().map(|c| c.map(|idx| remap[idx as usize]))).
Unification and promotion¶
unify_variants and promote_to_variant are the only places that change storage type. merge.rs:extend and engines_to_record_batches call unify_variants(skey, okey) before extend_owned; if None, they return Error::Merge("column '{name}' has conflicting types ({skey} vs {okey}); provide explicit field_types"). Promotions are Int64 -> Float64 via take plus map as f64, and String -> Dictionary via rebuilding dict/index/codes. Same key is a no op.
Finish¶
TableBuilder::finish (also ColumnarSink::finish) does:
normalize(truncatelen > row_countand clearrow_dirty)- Early
new_emptyifcolumn_orderempty auto_dict_upgrade(ifplan.auto_dict)sort_columnsbyschema_order- Build
fieldsplusarraysby iteratingcolumn_orderandget_column(name)to callarrow_datatypeandto_arrow_array Schema::new(fields)plusRecordBatch::try_new
No borrowed bytes outlive finish; StrColumn owns its data and numeric Vecs are owned.
Compare filter reapplication¶
arrow_export::apply_compare_filter exists for callers filtering an already built RecordBatch (for example merging). It is not the per row filter. Per row FilterPredicate::check is authoritative; apply_compare_filter only reapplies pure Compare and And of Compare via Arrow compute (compare_columns casts both to Float64 if numeric else Utf8, then gt, lt, gt_eq, lt_eq, eq, neq, and and for And, plus filter_record_batch). Trees containing Or, Not, Equal, or NotEqual are returned unchanged to avoid double null semantics. This matches the comment in arrow_export.rs:24.
Error mapping¶
StrColumn::to_arrow can return ArrowError (offsets, null buffer). to_arrow_array for dictionary can return ArrowError for DictionaryArray::try_new. Both surface as Error::Arrow and, via rypipe-python, as PyException with Arrow error: .... Utf8 from simdutf8 surfaces as ParseError in Python.