Reads and writes rows and Arrow batches on any yggdryl handle - Arrow IPC/Feather, Parquet, Avro, plain-text logs, Iceberg tables and hive-partitioned folders - with streamed readers, pushdown, append/merge (upsert) and commit cadence. Use when calling read_arrow_reader / readArrowReader, overwrite_arrow_* / append_* / merge_*, write_arrow_* with a mode, *_records / readRecords, read_arrow / write_arrow (SerieReader), read_arrow_field / row_size (a file's schema or row count without reading it), pandas or polars frames to and from a file (read_polars_frame, overwrite_pandas_frame, scan_polars), RecordOptions (field, safe, select, filter, merge_by, max_row_size, row_offset, commit_row_size, plan), TextOptions rowheader, iceberg Table create/append/merge/scan, partition pruning, or picking an encoding by suffix. Covers Rust, Python and Node.js.
Resources
1Install
npx skillscat add platob/yggdryl/yggdryl-records Install via the SkillsCat registry.
Records
IOMedia is the record surface every handle answers: one streamed read
(read_arrow_reader) and three explicit write intents (overwrite, append,
merge) in three input shapes (a batch reader, one batch or table, native
rows). The handle's media type - its suffix or declared MediaType - picks
the encoding; one RecordOptions carries every setting, and no call takes a
format argument. A schema is a non-null Struct Field; a read hands back a
stream in which only the current batch is alive.
Hold one model: the options are the query. field declares and casts,select projects, filter prunes (row groups, partition leaves, Iceberg
manifests) and then filters rows, max_row_size stops pulling, merge_by
keys an upsert, commit_row_size bounds a write. Put them on the call and the
medium does the work before a byte is decoded.
Choose the door
| Task | Rust | Python | JavaScript |
|---|---|---|---|
| options for this handle's encoding | handle.record_options()? |
handle.record_options() |
handle.recordOptions() |
| options from a name or type, no handle | RecordOptions::for_media_type(&url.media_type())?, RecordOptions::for_mime_type(&MimeType::PARQUET)? |
RecordOptions("trades.parquet") |
RecordOptions.from('trades.parquet'), RecordOptions.forMimeType(MimeType.ARROW_STREAM) |
| stored schema, no rows decoded | read_arrow_field(&options)? |
read_arrow_field() |
readArrowField() |
| row and column counts from metadata | row_size()?, column_size()? |
.row_size, .column_size |
.rowSize, .columnSize |
| stream batches out | read_arrow_reader(&options)? -> arrow::BatchReader |
read_arrow_reader() -> pyarrow.RecordBatchReader |
readArrowReader() -> BatchReader of Arrow JS batches |
| stream record columns out | read_arrow(Some(&options))? -> SerieReader |
read_arrow() -> SerieReader |
not bound |
| rows out as native values | read_arrow + serie.child(name) / scalar(i) |
read_records(), read_records(Cls) |
readRecords(), readRecords(Cls) |
| replace | overwrite_arrow_reader(reader, &options)?, overwrite_arrow_batch |
overwrite_arrow_reader, _table, _batch |
overwriteArrowReader(BatchReader.from(x)), overwriteArrowTable, overwriteArrowBatch |
| append | append_arrow_reader, append_arrow_batch |
append_arrow_reader, _table, _batch |
appendArrowReader, appendArrowTable, appendArrowBatch |
| upsert by key | merge_arrow_reader(r, &options.with_merge_by(["id"])?)? |
merge_arrow_table(t, merge_by=["id"]) |
mergeArrowTable(t, { mergeBy: ['id'] }) |
| mode chosen at run time | write_arrow_reader(r, IOMode::Append, &options)?, write_arrow_batch, write_records |
write_arrow_table(t, "append"), write_arrow_reader, write_arrow_batch, write_records |
writeArrowTable(t, 'append'), writeArrowReader, writeArrowBatch, writeRecords |
| native rows in | overwrite_records(rows, &options)? (rows Into<Scalar>) |
overwrite_records([dict or @scalar instance]) |
overwriteRecords([object], { field }) |
a SerieReader or held column in (also JSON/YAML/TOML/XML rows) |
write_arrow(SerieReader::from_serie(s)?, IOMode::Overwrite, None)? (a document handle takes Overwrite only) |
write_arrow(value, "overwrite") (document handle: overwrite only) |
not bound |
| refuse values the declared field cannot convert | options.with_field(root).with_safe(false) |
read_arrow_reader(field=f, safe=False) |
readArrowReader({ field, safe: false }) |
| one setting for one call | options.clone().with_select(["id"])?.with_filter("id > 3")? |
read_arrow_reader(select=["id"], filter="id > 3") |
readArrowReader({ select: ['id'], filter: 'id > 3' }) |
| sections as one plan | options.with_plan("select id where x > 1 limit 5")? |
options.plan = "select ..." |
options.withPlan('select ...') |
| Parquet page codec | options.set_parquet_compression_name("zstd(3)")?, ParquetOptions::new().with_compression(..) |
compression="zstd(3)" |
{ compression: 'zstd(3)' }, withCompression |
| Parquet footer statistics | read_parquet_statistics()? |
read_parquet_statistics() |
readParquetStatistics() |
| Avro bytes with a reader schema | avro::read_container_resolved(&h, &schema)? |
avro.loads(data, reader_schema=...) |
avro.loads(data, { readerSchema }) |
| log lines as typed rows | handle.into_text_with(TextOptions) |
IOBase(p).into_text(TextOptions()), or read_arrow_reader(rowheader=...) |
new IOBase(p).intoText(opts), or readArrowReader({ rowheader }) |
| write a partitioned folder | Holder::folder(&root)?.overwrite_arrow_reader(r, &options)? |
IOBase(dir).overwrite_arrow_batch(b, options=o) |
new IOBase(dir).overwriteArrowTable(t, options) |
| leaves of one partition | children_where(&[("year", "2024")], false)? |
children_where({"year": "2024"}) |
childrenWhere({ year: '2024' }) |
| derived partition column | field.as_partition_mut().set_transform(Function::Year)?, root.as_transform().apply_arrow_batch(&b)? |
field.partition.transform = "year", root.partition.apply_arrow_batch(b) |
not bound |
| Iceberg table | Table::create(LocalFolder::new(p)?, FormatVersion::V2, schema, spec)? |
Table.create(IOBase(p), schema, ["venue"]) |
iceberg.Table.create(p, schema, ['venue']) |
| Iceberg write | commit_append(r)?, commit_overwrite, commit_merge(r, &sel, safe)? |
append(t), overwrite, merge(t, ["id"]) |
append(t), overwrite, merge(t, ['id']) |
| Iceberg filtered scan | scan_matching("px > 1", None)?, plan_matching(..)? |
scan_matching("px > 1"), plan_matching(..) |
scanMatching('px > 1'), planMatching(..) |
| Iceberg time travel | scan_at(snapshot_id, &[], None)? |
scan_at(snapshot_id) |
scanAt(snapshotId) |
| Iceberg schema change | SchemaUpdate::from_metadata(..)? + update_schema(&update)? |
update_schema().add_column("", f).commit() |
updateSchema().addColumn('', f).commit() |
| lazy engine scan | - | scan_polars(), scan_arrow() |
- |
| pandas / polars frames to and from a file | - | read_pandas_frame(), read_polars_frame() (whole), read_pandas() / read_polars() (lazy iterator of frames), overwrite_pandas_frame(df), append_polars_frame(df), merge_pandas_frame(df, merge_by=[...]), write_polars(frames, mode) |
- |
| SQL-like write/read plan | "select ...".parse::<Plan>()?.execute()?, apply_arrow_reader |
Plan("insert into ...").apply_arrow_batch(b), .execute() |
new Plan('...').applyArrowBatch(b), .execute() |
Rules for fast, correct use
- Keep the reader a reader. Iterate
read_arrow_reader, or hand it
straight to another handle'soverwrite_arrow_reader; only the current
batch is alive.read_all()/intoTable()/collectonly when the
caller asked for a whole table. - Push
select,filterand limits into the options. Parquet skips
unprojected column chunks and row groups whose footer statistics rule the
filter out; IPC skips decoding unprojected columns; a folder skips leaves
whosecolumn=valuepath contradicts a filter equality; Iceberg skips
manifests and files. Filtering rows after the read throws all of that away. - Declare the
fieldto cast once. A narrower field is a projection; a
wider one fills missing nullable columns with nulls; the cast runs in the
same pass as the decode. Anot nullcolumn refuses a value, a null or a
missing column by name - it never stores a default. A nullable declared
column takes a value it cannot convert as null whilesafeholds, andsafeis on by default - so a bad value vanishes silently. Passsafe=False/{ safe: false }/.with_safe(false)(or declare the
columnnot null) when an unconvertible value must be refused. merge_byis required for merge. Keys use Arrow's row format: null
matches null and the last arrival wins. Merge holds only the stored side in
memory;merge_byabsent is a refusal, never an overwrite.- Bound memory with
commit_row_size. Unset publishes once at the end;Npublishes everyNrows and the committed prefix survives a later
failure;0is refused before any input is pulled. row_size/column_size/read_arrow_fieldread metadata only. They
answer from a footer, a stream header or the manifests;open()caches the
answer untilclose().- The name picks the encoding and the outer coding.
.arrows(IPC
stream),.arrow/.feather/.ipc(IPC file),.parquet,.avro,.txt/.log, a table folder;.gz,.zz,.zstwrap the bytes.
Parquet compresses internally, so.parquet.gzis refused before a byte is
written - setcompressioninstead. - Parquet and Iceberg are Cargo features in Rust (
parquet,iceberg
impliesparquet); the Python and Node packages carry both. - Absent is empty; never probe first. An absent resource reads as no
batches and the first write creates it and its parents. Do not guard withexists; an encoding the build lacks (e.g.text/csv) is named byrecord_options(). - Properties by name are copies.
read_arrow_reader(rowheader=...)/readArrowReader({ rowheader })set that property on a copy of the
handle's (or the given) options; the handle's options are unchanged. Left
out (.../undefined) keeps the default;None/nullclears. - JavaScript crosses as copied IPC. One self-contained IPC stream per
batch; write whole tables or readers, never rows in a loop. Python crosses
the C Data Interface without copying. - One plan per stream. A declared field or plan is compiled once per
read or write session; build the options once and reuse them across
calls instead of re-parsing a filter per batch. - Pick the input shape that is already in hand. A reader streams; a
table or batch is wrapped into one; native rows (*_records) cross the
value contract row by row - a row that omits a required column is refused,
not defaulted - and cost the most (4,096 rows: about 0.1 ms as
a batch, 3 ms as records, on the docs' reference machine). Keep rows for
small or hand-built data, batches for everything else. - Plain text has a fixed shape. A text read answers the sixteen event
columns (currunixfirst,statelast), thenbody, then one column per
namedrowheadercapture - the row header is the only thing that lifts a
column out of a line.autotypesettles each capture's datatype from the
regex before a byte is read. An object's lines are one chain: a line whose
owncreaunixcapture states none takes the earliestcurrunixthe read
has dated a line of its object by so far; acreaunixcapture stands. A write consumes each row's non-emptybody. - Iceberg commits are snapshots.
appendand metadata-only commits
rebase on a concurrent commit;overwrite,mergeandcompactreport a
conflict instead. A merge keys on the identity partition columns plusmerge_by, so a row only ever updates its own partition. A scan decodes
qualifying files side by side (read.parallelism) and hands batches back
in plan order. - Folders read by their layout. A stored
column=valuelayout is
authoritative; with none on disk, the schema's partition-marked fields
decide where rows go (with_partition_fields). The first batch to reach a
leaf performs the write's operation; later ones append.
Pitfalls
- Reading then filtering in the host (
[r for r in rows if r["id"] > 3],table.filter(...)) - passfilter="id > 3"so the medium prunes. merge_*with nomerge_by- it raises; passmerge_by=["id"]/{ mergeBy: ['id'] }/with_merge_by(["id"])?.- Naming a file
trades.parquet.gz- refused ("parquet compresses"); usecompression="zstd(3)"on a plain.parquet. - Skipping rows in the host after the read:
row_offset(rowOffset,with_row_offset) skips leading rows beforemax_row_sizecounts, and a
plan'soffsetlands there too (options.plan,withPlan,with_plan,execute). A batch the skip covers whole is never handed on; a merge with
arow_offsetis refused. - Calling
overwrite_records/read_arrow_readeron a.json,.jsonl,.yaml,.tomlor.xmlhandle - refused ("expected a record encoding
this build implements"); usewrite_arrow/read_arrow(Rust, Python) or the codecs inyggdryl-documents. - Appending rows to a
.json/.jsonl/.yaml/.toml/.xmlhandle withwrite_arrow(..., append)- refused: a structured document is written
whole, so onlyoverwriteis accepted; to accumulate rows use an.arrows,.parquetor.avrohandle, or read, extend and overwrite. - Declaring
int32over a column holding"x"and trusting the read: under
the defaultsafea nullable column reads it as null. Passsafe=False/{ safe: false }/.with_safe(false)to be told. - JavaScript plain-object rows with strings are inferred as
dictionary(int32,utf8); every encoding stores them (Avro as the plain
values), but pass{ field }to state the column rather than inherit Arrow
JS's guess. - Python:
read_arrow_reader(options)positionally is aTypeError; options
are keyword-only (options=). - Reading one leaf of a partitioned folder and expecting the partition
columns: they live in the path; read the folder. - Reusing an Iceberg
Tableobject after writing through another handle: it
caches metadata; open the table again. - Collecting a Parquet read to count rows or learn the schema:
row_size
andread_arrow_fieldanswer from the footer. commit_row_sizeon an Iceberg write: every commit is a snapshot, so a
smallNleaves many snapshots; expire them (expire_snapshots) or commit
once.- Building an Arrow JS
Int64vector fromnumbers: usebigint(1n).
Native row writes unifynumberandbigintrows of one column instead:[{ id: 1 }, { id: 2n }]is oneint64column, the integralnumberread
asbigint; only a fraction besidebigintrows is refused, naming the
column and the value. - Looking for a CSV reader:
text/csvis not a record encoding; read lines
with arowheaderregex, or convert upstream.
Language references
references/rust.md- read for Rust: traits to import,RecordOptionsbuilders, batch readers, IcebergTable.references/python.md- read for Python: keyword properties, pyarrow readers, dataclass rows, lazy scans.references/javascript.md- read for Node.js: property objects, Arrow JS tables,bigint, copied IPC.references/formats.md- per-encoding table: media type and suffix, settings, pushdown, feature gate, limits.
Deeper
- Records surface (signatures, pushdown, limits, append and merge, commit cadence, lazy scans): https://platob.github.io/yggdryl/holder/#records
- Partitions (pruning, partition columns, derived columns): https://platob.github.io/yggdryl/holder/#partitions
- Media overview and options: https://platob.github.io/yggdryl/media/#read-and-write
- Per format: https://platob.github.io/yggdryl/media/#arrow-ipc, https://platob.github.io/yggdryl/media/#parquet, https://platob.github.io/yggdryl/media/#avro, https://platob.github.io/yggdryl/media/#plain-text, https://platob.github.io/yggdryl/media/#iceberg
- Required columns and the cast rule: https://platob.github.io/yggdryl/types/cast/
- Plans and write verbs: https://platob.github.io/yggdryl/expression/plans/
- Sibling skills:
yggdryl-storage(handles, backends, codings),yggdryl-arrow(Serie,SerieReader, casts),yggdryl-expressions(filter/select grammar,Plan),yggdryl-uri(hive paths, globs),yggdryl-types(fields, dataclasses),yggdryl-documents(JSON/YAML/TOML/XML).