arrow-mlake · API

arrow_mlake.carrow_import

The Arrow C Data Interface, inbound — another runtime's arrays read into this one.

arrow_mlake.carrow is the outbound half: an ArrayData tree becomes a C ArrowSchema/ArrowArray pair that pyarrow, DuckDB or a Rust crate can consume. This module is its inverse. import_c turns that same pair back into an ArrayData in an ArrayArena, and ImportedStream drives the third struct of the interface, ArrowArrayStream, which is how a producer hands over a sequence of batches:

struct ArrowArrayStream {
  int (*get_schema)(struct ArrowArrayStream*, struct ArrowSchema* out);
  int (*get_next)(struct ArrowArrayStream*, struct ArrowArray* out);
  const char* (*get_last_error)(struct ArrowArrayStream*);
  void (*release)(struct ArrowArrayStream*);
  void* private_data;
};

That is what a scan returns, so it is the entry point a LanceDB or ADBC binding actually needs.

Importing copies. ArrayData owns its buffers as List[UInt8], so every buffer a producer hands over is copied into Mojo memory. The alternative — an ArrayData that borrows a foreign pointer and keeps the producer's release callback alive behind it — would be zero-copy but changes the type every consumer of ArrayData reads, so it is deliberately not what this does. On a 16-column, 65k-row batch the copy runs at memory-copy speed, which is a rounding error next to decoding the same data out of a file format.

Who releases what. The C Data Interface makes the consumer responsible for the structs it is handed: it calls the root's release, which frees the whole tree, and a released struct is recognised by a null release pointer. Children are never released on their own — the root owns them. This module implements exactly that side of the convention:

  • import_c borrows. It reads the pair and copies out of it, and it does not release anything. A caller that owns the pair — because a producer moved it into caller storage — decides when to release it.
  • ImportedArray owns. Constructing one asserts "this pair is mine now"; it releases the root array and the root schema exactly once, on release() or on destruction, and release() is idempotent because it checks for the null release pointer first.
  • ImportedStream owns the stream, the schema it pulled out of get_schema, and every array get_next moves into it. Each array is released as soon as its batch has been copied, so a long scan holds one batch of producer memory at a time and not the whole stream.

Nothing about a producer is trusted. n_buffers is checked against the format string, n_children against the type, offsets for monotonicity and against the child length they index into, and every format string this module cannot name is an error carrying the string itself. The interface does not carry buffer sizes — a const void** and nothing more — so a truncated utf8 data buffer is undetectable by construction; what can be cross-checked is checked, and the limit is stated rather than papered over.

var arena = ArrayArena()
var root = import_c(arena, array_addr, schema_addr)   # borrowing
var batch = ImportedArray(array_addr, schema_addr).into_batch()  # owning

Structs

ImportedArray

struct ImportedArray

One C ArrowArray/ArrowSchema pair, owned by us until released.

Construct this when a producer has moved the two structs into storage you hold — which is what "consuming" means in the C Data Interface — and it will call the root release callbacks exactly once, on release() or at destruction. The addresses stay valid afterwards, but their contents do not, so import before releasing.

Implements Movable

Fields
array: Int: Address of the ArrowArray.
schema: Int: Address of the ArrowSchema.

__init__

def __init__(out self, array: Int, schema: Int)

__deinit__

def __deinit__(deinit self)

into_arena

def into_arena(self, mut arena: ArrayArena) -> Int

into_batch

def into_batch(self) -> RecordBatch

release

def release(mut self)

CArrowArrayStream

struct CArrowArrayStream

The C ArrowArrayStream, field for field. Pointers are addresses.

Implements Copyable, Movable

Fields
get_schema: Int
get_next: Int
get_last_error: Int
release: Int
private_data: Int

ImportedStream

struct ImportedStream

A producer's ArrowArrayStream, consumed batch by batch.

This is the shape a scan arrives in: one schema up front, then arrays until the producer signals the end by handing back a released array — a null release pointer and nothing else. get_next returning non-zero is a real error, and get_last_error says what it was; the two are distinguished here because conflating them turns a failed scan into a short one.

Ownership runs three ways and all three are ours. The stream is released when this object dies. The schema is moved out of get_schema into storage this object allocates, and released with it. Each array get_next moves into us is released as soon as its batch has been copied, which is what keeps a long scan to one batch of producer memory at a time rather than the whole stream.

Implements Movable

Fields
stream: Int: Address of the caller's ArrowArrayStream.

__init__

def __init__(out self, stream: Int)

Take ownership of stream and pull its schema.

__deinit__

def __deinit__(deinit self)

format

def format(self) -> String

The root format string of the stream's schema, usually +s.

schema_address

def schema_address(self) -> Int

next

def next(mut self) -> Optional[RecordBatch]

The next batch, or nothing once the stream has ended.

release

def release(mut self)

Functions

release_c_array

def release_c_array(addr: Int)

Call a producer's release on an ArrowArray, once.

A released struct carries a null release, which is both how the producer tells us it is gone and how this stays idempotent. Only ever called on a root: the interface says children are freed by their parent, and calling a child's release does nothing but mark it.

release_c_schema

def release_c_schema(addr: Int)

Call a producer's release on an ArrowSchema, once.

parse_format

def parse_format(fmt: StringSpan) -> ArrowType

One Arrow C Data Interface format string as an ArrowType.

Every string this library can write round-trips, plus the temporal and decimal spellings a foreign producer will send that our writer never emits. Everything else raises with the string in the message: guessing at an unrecognised format would mean reinterpreting somebody else's buffer, which is the one failure mode that produces wrong numbers silently rather than an error.

n_children_for_type

def n_children_for_type(id: Int) -> Int

How many children a type must have, or -1 where any number is legal.

extension_name

def extension_name(metadata: Int) -> String

ARROW:extension:name out of a C schema's metadata block, if present.

The encoding is Arrow's own: an int32 count, then that many (int32 length, bytes) pairs alternating key and value, native-endian and unaligned. Nothing in the interface bounds the block, so the counts are sanity-checked before they are used as loop bounds — a garbage n here would otherwise be an unbounded read of somebody else's address space.

parquet_field_id

def parquet_field_id(metadata: Int) -> Int32

PARQUET:field_id out of a C schema's metadata, or -1.

Arrow's Parquet writers put the Iceberg/Parquet field id here as decimal text, and this library's ArrayData has a slot for it. Our own exporter does not write the key, so a round trip through it drops the id; a foreign producer that does write it keeps it.

import_c

def import_c(mut arena: ArrayArena, array: Int, schema: Int) -> Int

Copy a foreign C array into arena; return the index of its root.

Borrowing: the pair is read, never released. The caller owns it — see ImportedArray for the owning form.

import_batch_c

def import_batch_c(array: Int, schema: Int) -> RecordBatch

A foreign C array as a RecordBatch.

A stream's arrays are struct arrays by definition — the struct's fields are the batch's columns — so a +s root is unwrapped into one root per field. Anything else becomes a one-column batch, which is what a caller importing a bare column wants.

A struct root with nulls of its own has no representation here, because a RecordBatch is a list of columns and not an array; that raises rather than dropping the row-level nulls on the floor.

Aliases

comptime MAX_IMPORT_DEPTH = 64

How deep a foreign type may nest before we call it a malformed producer.

Children are reached through pointers a producer wrote, so a cycle is possible and would otherwise be an unbounded walk. Real Arrow schemas are a handful of levels deep; anything past this is a bug on the other side of the interface.

comptime StreamGetSchemaFn = def(Pointer[CArrowArrayStream, MutUntrackedOrigin], Pointer[CArrowSchema, MutUntrackedOrigin]) abi("C") thin -> Int32
comptime StreamGetNextFn = def(Pointer[CArrowArrayStream, MutUntrackedOrigin], Pointer[CArrowArray, MutUntrackedOrigin]) abi("C") thin -> Int32
comptime StreamGetLastErrorFn = def(Pointer[CArrowArrayStream, MutUntrackedOrigin]) abi("C") thin -> Int
comptime StreamReleaseFn = def(Pointer[CArrowArrayStream, MutUntrackedOrigin]) abi("C") thin -> None