blob: c57a6144a6bc8137802567b81c2763823cc24221 [file] [view]
<!---
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
# The Arrow.jl engine: ArrowCore and its adapters
Arrow.jl is a runtime-tagged, C-data-shaped core (`ArrowCore`, private) with
the IPC reader/writer, the C data and C stream interfaces, and `Tables.Scan`
pushdown as peers over it, and `Arrow.Table`/`Arrow.Stream`/`Arrow.write` as
the public facade on top. `ArrowCore` depends only on Base and the Mmap
standard library. This document is the design rationale and the exact
scope of every layer.
## Files
| File | Purpose |
|---|---|
| `src/ArrowCore.jl` | Ownership regions with one revocation cell, runtime descriptors, `Field`/`Schema`, `ArrayData`, the layout registry, staged validation, dynamic and typed accessors, bulk fixed-width extraction, minimal builders, `RecordBatch`, and `RecordBatchSource` |
| `src/metadata/` | FlatBuffers metadata bindings and the shape verifier, both generated by `tools/fbsgen.jl` from the vendored `src/metadata/fbs/*.fbs`, over the schema-blind `VerifierRuntime.jl` |
| `src/FlatBuffers/` | The vendored FlatBuffers runtime (table reads, builder) |
| `src/ipc_read.jl` | Checked IPC stream framing, resource limits, metadata-to-Core mapping, dictionary state, one registry-driven decoder, per-buffer decompression |
| `src/ipc_write.jl` | The write half over the same registry: Core-to-metadata mapping, one generic registry-driven encoder, replacement-on-change dictionary batches, per-buffer compression, the file format (Block index + Footer), and the lazy random-access `ArrowFile` reader |
| `src/cdata.jl` | C data and C stream interfaces both directions: zero-copy ownership, move semantics, exactly-once release, field and schema metadata transport |
| `src/source.jl` | The `AbstractArrowSource` byte-range source interface (`sourcelength`, `readrange`, `concurrentreads`) |
| `src/scan.jl` | The private scan-plan module: one-time binding and lowering, exact execution and schema projection, sparse byte-range reads over `SourceFile`, and embedded per-batch statistics. `table.jl` includes it at the public/storage type seam. |
| `src/table.jl` | The read facade, including the shared public/storage type seam used by scan lowering and retained construction |
| `src/columnconstruction.jl` | The deep column-construction module: fresh inference, retained-schema reconstruction, recursive ArrowTypes lowering, shared dictionary pools, partition agreement, and field metadata behind `_constructcolumn` |
| `src/write.jl` | The write facade: partition binding, retained-field alignment, schema and batch assembly, compression selection, and IPC emission |
| `ext/ArrowCloudStoreExt.jl` | CloudStore.jl objects as sources: HTTP `Range` reads, concurrent per planned range |
| `src/ArrowStrings/` | ArrowStrings.jl — the shared inline-else-view string representation (`ArrowString`, `StringVector` = Utf8View memory); a separate package, registered on its own like ArrowTypes, that Arrow depends on through a `[sources]` path entry until its first release |
| `src/ArrowTypes/` | ArrowTypes.jl — the separate custom-type interface package; the facade applies its lowering and extension hooks recursively |
| `test/support/AcceptanceSupport.jl` | One explicit private dependency seam for the four stable adapter acceptance suites; the C Data stress child uses its own narrow support module |
| `test/support/SeededFuzz.jl` | Version-stable differential, layout-family, ranged-read, statistics-pruning, and bounded mutation fuzzing with exact replay artifacts |
| `test/` | Core and facade tests, ArrowTypes compatibility, support contracts, the four adapter acceptance batteries, frozen 2.x-written fixtures, and the `--trim=safe` gate |
| `conformance/ConformanceSupport.jl` | Strict integration-JSON shape validation, logical-value canonicalization and comparison, skip policy, verdict construction, and reporting shared by every conformance adapter |
| `conformance/` | The arrow-testing gold-corpus runner, the integration-JSON implementation, the pyarrow/nanoarrow IPC oracle, and the in-process pyarrow C Data / C Stream oracle |
| `bench/` | The serialize/deserialize benchmark harness (this package and PyArrow) |
| `docs/dev/DESIGN-scan-ranges-trim.md` | The scan pushdown, ranged-fetch, and statistics design |
## Run it
On Julia 1.10, prepare a fresh checkout once before you run the commands below.
Julia 1.10 does not read the repository's `[sources]` entries.
```bash
julia --project=. -e 'using Pkg; Pkg.develop([PackageSpec(path="src/ArrowStrings"), PackageSpec(path="src/ArrowTypes")])'
```
```bash
julia --project=. -e 'using Pkg; Pkg.test()' # core + facade + batteries
julia --startup-file=no test/trim_compile_tests.jl # JuliaC --trim=safe gate
julia --startup-file=no conformance/run.jl # all conformance suites, in the docker image
julia --startup-file=no conformance/run.jl corpus # one suite: the gold corpus …
julia --startup-file=no conformance/run.jl oracle # … IPC bytes through pyarrow + nanoarrow …
julia --startup-file=no conformance/run.jl cdata # … C Data + C Stream through an in-process pyarrow
julia --project=. test/fuzz.jl --cases 16 --mutations 64 # deterministic PR-sized fuzz suite
julia --project=. test/fuzz.jl --cases 512 --mutations 20000 --determinism-every 256 --repro-dir fuzz-reproductions # extended suite
julia --project=. bench/run.jl # benchmarks
julia tools/fbsgen.jl src/metadata/fbs src/metadata # regenerate bindings + verifier
```
For a chosen master seed, a fixed SplitMix64 stream derives every case. The
master seed and case index identify the same input on every supported Julia
version. PR tests and manual runs default to one fixed seed. Each scheduled run
uses its workflow run ID as a new reproducible seed. Active-case artifacts are
written before parser work. They record both seeds, the source revision, exact
generated and saved-byte commands, and the active Project and Manifest with a
restore command. Mutation replays compare two full outcomes. The scheduled
workflow also repeats the first complete mutation-route sweep and every 256th
later mutation. It stops the fuzz process before the job timeout, then uploads
the active artifacts even for a hang or forced stop. After download, run the
artifact's `replay.sh` with a clean Arrow.jl checkout path. The wrapper resolves
its own artifact directory, temporarily installs and instantiates the recorded
Project and Manifest, runs the replay, and restores the checkout files.
`Tables.Scan` pushdown needs the Tables.jl revision pinned in `Project.toml`
developed into the project environment (the conformance image clones it). The conformance
suites run inside one docker image (`conformance/Dockerfile`: Julia, a
Python with pyarrow and nanoarrow that PythonCall binds to, the
apache/arrow-testing corpus, the pinned Tables revision, a warm depot in a named
volume) driven by `conformance/run.jl` through Harbor.jl. The driver
instantiates its own tiny host environment (`conformance/host/`, Harbor.jl
only) on first run, so docker — and network for that first run — are the
only host requirements; `conformance/Project.toml` is the IN-CONTAINER suite
environment. The C interfaces hand pointers across an in-process
boundary, which is why the suites run in the container rather than against
one.
## Design in one table
| Principle | Where it lives |
|---|---|
| Ownership is an object; bad spans fail before access | `OwnerRegion`, checked `BufferSlice` construction, bounds-checked `loadat`. Foreign C extents are trusted declarations. |
| Validity is reachability plus one revocation bit | The memory model below. |
| Logical parameters are values, not type parameters | `TimestampType(unit, timezone)`, `DecimalType(precision, scale, bitwidth)`, and every other descriptor keep schema data out of Julia types. |
| One structural registry plus bounded per-layout methods | `layoutspec` defines buffer roles, child arity, offset width, and variadic status; access and semantic rules are grouped methods. |
| Staged validation | Structural, then semantic, then the opt-in full tier; each public stage composes the earlier ones. Data-intrinsic semantic results are cached; Field contracts run every time. |
| Framing enforces limits before allocation | The IPC framer enforces metadata, body, message, and allocation limits; the generated verifier bounds objects, depth, and copy reserve; the decode cursor enforces array and buffer limits before the related work. |
| The message body is the decode authority | Every declared batch buffer is a checked `subslice` of its own message body; cursor completion and non-overlap checks reject skewed buffer tables. |
| IPC ids are adapter state | `corefield` records ids in identity-keyed adapter tables; `DictionaryType` holds the value type and `ArrayData.dictionary` the value array; neither stores an id. |
| C Data is a direct mapping over `ArrayData` | `to_c_data`/`from_c_data` use per-structure callbacks and controls, separate schema/array roots that keep sources reachable, transitive release, and explicit reaping. |
| Function-barrier bulk access | `materialize` resolves the layout once and loops; scalar `getvalue` pays one dynamic dispatch. Static claims through `getvalue(::Type{T}, …)`/`materialize(::Type{T}, …)` resolve statically. |
## Memory model
Buffer validity is GC reachability, plus one revocation bit. An
`OwnerRegion` is a `(ptr, len, alignment, root, cell)` record: `root` is an
opaque GC anchor (the wrapped `Vector`, the Mmap-stdlib array, or an
adapter's owner object), so holding any slice of a region keeps the backing
memory alive by construction; `cell` is a `ReleaseCell` shared by every
region over one underlying lifetime. Loads are a bounds check, one monotonic
closed-flag load, and the raw read — no lock, no guard, no state machine on
the hot path.
`release!` is the deterministic release: it revokes every region sharing the
cell (later raw access throws `InvalidStateException`) and then runs the
cell's release action exactly once. An mmap region unmaps NOW (the eager
path exists for hosts where a GC-timed unmap is not enough — deleting a
still-mapped file on Windows being the canonical case); an imported C-data
tree runs the producer's release callback; a borrowed heap region is revoked
with no eager action (running a borrowed object's finalizers is not ours to
do). `release!` is idempotent and is not a data-race shield for accesses
concurrent WITH the close — quiescing readers first is the caller's
contract, as with `Base.close` on a shared IO. Every buffer imported from
one C-data tree is backed by regions sharing one cell, so closing any of
those regions, or `release!` on the import's owner, revokes all siblings
before the single producer release.
What the model does not do: nothing prevents external writes to or
truncation of a mapped file while the mapping or cached validation results
are in use (no userspace scheme can). On systems that prohibit deleting an
active mapping, `release!` (or collection) must complete before the path can
be deleted.
Exactly-once release lives in the adapters' owners: the C-data
`ForeignOwner` and C-stream `StreamOwner` carry one `@atomic` flag each, a
finalizer, and an explicit `release!`; their revocation cells route through
the same flag. The export registries root exported columns and streams until
the consumer releases them and cleanup drops the root.
## Scope and limits, layer by layer
### Core
Accessors and validation cover every format-1.5 layout: integer, floating
point, Boolean, decimal (32/64 as integers, 128/256 as raw native-endian
bytes), date, time, timestamp, duration, all interval variants, UTF-8 and
binary with 32-bit or 64-bit offsets, Utf8View and BinaryView (16-byte
entries, inline and out-of-line, variadic data buffers, the spec's
prefix-must-match rule), fixed-size binary, list, fixed-size list, ListView
and LargeListView (per-slot offsets and sizes, unordered and overlapping
ranges, invariants binding null slots too), struct, map, sparse and dense
union over the full Int8 id domain, run-end encoding (signed 16/32/64
no-null strictly-ascending run ends, binary-search access, logical nulls
through the values child, parent null count zero or unknown), dictionary,
and null arrays. Logical parent offsets and nested slices are supported.
Struct scalars on the dynamic path are ordered `Vector{Pair{String,Any}}`,
so names stay in the value domain and duplicate, empty, or
non-Symbol-compatible names are representable. Core keeps every schema field
and child name as a `String`; it does not intern schema names. The typed path
(`getvalue(::Type{T}, field, data, i)`, `materialize(::Type{T}, field,
data)`) is a caller-asserted element domain: exact match only (no
conversion; `Missing <: T` admits nulls; `Any` is the dynamic path),
composites recurse (List → `Vector{E}`, Struct → `Vector{Pair}` or a
NamedTuple with name checks, Dictionary → pool values, run-end encoding
transparent), unions refuse every static claim, and claims are checked
against the descriptor before any element is read. Closed fixed-width
claims over Int/Float/temporal/Decimal32/64 columns use one bounds-checked
bulk byte copy plus a bitmap null punch.
`validate_full` adds UTF-8 well-formedness for Utf8 and Utf8View, the
advisory nullability contract, Date64 day divisibility, time-of-day range,
decimal precision, and canonical bit-packed form (zeroed trailing bits and
padding in validity/Bool buffers; unsliced arrays only, since sliced windows
legitimately share bitmap bytes). On-wire buffer padding is a writer
guarantee, not a reader requirement. Map validation checks physical layout
and reachable Field nullability; it does not check key uniqueness,
hashability, or ordering — `keysSorted` is a producer declaration.
Timestamp validation checks the unit domain and timezone-string UTF-8; it
does not resolve names against a timezone database. `RecordBatch` buffers
must be host-native endian (the IPC adapters refuse big-endian input; no
adapter normalizes). Julia vectors wrapped zero-copy by the builders or `heapregion` are
scoped borrows: they must not be resized or mutated while their `ArrayData`
or cached validation results are in use.
`fromviewentries` wraps a vector of Arrow view entries (ArrowStrings'
`ArrowStringPayload`, or any 16-byte isbits type with that layout) and
its data buffers as a Utf8View column, zero-copy — the payload vector IS the
views buffer and every data buffer is retained by identity; only the
validity bitmap is built, and long-entry geometry (offsets inside their
buffer, prefixes matching the data) is checked by semantic/full validation,
not at construction. The facade's `Arrow.write` routes
`ArrowStrings.StringVector` columns through it.
### IPC
The reader maps every layout above, including nested dictionary encoding
(read and written in dependency order). It accepts V4 and V5 metadata on
little-endian hosts, supports feature-gated full dictionary replacement,
preserves old dictionary snapshots, and rejects delta dictionaries. It
requires the eight-byte continuation-marker framing (the pre-0.15 four-byte
prefix is not accepted). Compression uses the V5 `BodyCompression` field
for LZ4_FRAME and ZSTD; it accepts the `COMPRESSED_BODY` schema feature and
also accepts V5 compressed streams from Arrow.jl 2.x that omit it; it
rejects `BodyCompression` under V4 and the pre-1.0 experimental V4
compression marker. Big-endian streams are refused (no endianness
normalization).
Compatible fields that share one IPC dictionary id share one immutable pool
object; eager stream decoding fully validates each pool snapshot once and
reuses that identity certificate for structural, intrinsic, and
Field-contract validation while still checking each field's index array
independently — validation work is linear in the encoded indices plus
distinct pool data. The reader runs structural and semantic validation
before exposing a batch and does not opt into `validate_full`; the
generated verifier does validate FlatBuffer strings. The framer refuses a
non-little-endian host before any generated getter runs.
`readstream` decodes a borrowed `Vector{UInt8}` eagerly (raw batch buffers
are zero-copy views; positively compressed buffers are exact-sized owned
copies) behind the `RecordBatchSource` pull interface; the caller must not
mutate or resize the vector while the stream or its batches live.
`IPCStream` is a single-owner cursor — overlapping `nextbatch!` calls throw
`ConcurrencyViolationError`. `max_total_allocated_bytes` is one
reader-wide, conservative budget for metadata copies, metadata-directed
Julia containers, decompressed outputs, Core materialization, ArrowTypes
route containers, and facade copies. `IPCStream` carries the remaining budget
into `Arrow.Table` and `Arrow.Stream`; it is not a measurement of custom user
hook allocations or every Julia allocation. Package-owned vector reserves
include conservative backing capacity because Julia can round the requested
payload to a larger allocation class. Schema and Field metadata stay as ordered pair
vectors, so duplicate keys and their original order survive IPC reads and
rewrites.
The writer covers the same layouts with one registry-driven encoder, the
declared inverse of `decodefield`. It writes V5 stream bytes and the file
format (magics, Block indexes, Footer) with per-buffer LZ4_FRAME/ZSTD
compression behind the Int64 prefix and the `-1` stored-raw fallback.
Dictionary handling is replacement-on-change (one batch per pool snapshot;
`Feature.DICTIONARY_REPLACEMENT` declared when a replacement is emitted;
`COMPRESSED_BODY` when a compressed batch is). Files declare the
compression feature in both schema copies and refuse pools that change
identity across batches. Every column is semantically validated before its
bytes are published. The writer is eager and sequential (byte vectors,
buffer contents copied into message bodies); arrays with a nonzero element
offset are refused (materialize first); each schema position must be a
distinct `Field` object; fresh dictionary ids are assigned per field, and a
caller-supplied id table makes shared ids write as one shared dictionary
batch with value-schema compatibility, one nested-id topology per repeated
id, and one pool per id within each batch enforced before bytes are
emitted. Canonical empty offset arrays materialize their terminal zero on
the wire (the reader accepts the omitted form other writers emit).
`readfile` verifies both magics, the leading and footer schemas, cumulative
footer work, and every Block's frame, message kind, wire-buffer extents, and
overlap before optional-EOS classification; `ArrowFile` decodes record
batches lazily by footer index — each `getindex` runs with a fresh
allocation budget and codec contexts over the shared, eagerly-decoded
dictionary set, so concurrent reads need no coordination.
### Scan pushdown and ranged reads
`Tables.scan(::ArrowFile, scan)` and file/ranged `Arrow.Table(source; scan=…)`
share the same batch kernel through separate closed direct and facade
operations. They decode only the selected and filter-referenced columns,
prune whole batches through the embedded statistics (one-sided: a pruned
batch is provably empty), and consume the
scan exactly: without a filter `limit`/`offset` are metadata arithmetic and
batches outside the window are never decoded; with one the `_ScanSink`
evaluates the filter per batch through the generic evaluator
(`Tables.filtermask`), composes the window over the qualifying rows, and
stops decoding once it is full. Each request is resolved once. Projection
and renames are applied at column construction. Direct handle scans apply
type overrides in the storage domain. The facade applies them after public
conversion. The direct `_applyscan` seam is storage-only. The facade's
`_applyfacadescan` operation owns route-aware ArrowTypes Union materialization,
public conversion, and wrapping, so its private child markers cannot cross
back into `table.jl`. Stream facade scans keep the same route local but use
`_executeplan` after decode. `SourceFile` runs the same plan over an
`AbstractArrowSource`:
it uses the Footer (normally from one cached tail read, with one exact cached
follow-up when the Footer escapes that window) as its sole schema authority,
validates the full Block index and the complete metadata plan for every
statistics-surviving record before
requesting a body range, and requests per-buffer body ranges for exactly
the decode set, coalesced under `coalesce_gap`. It does not fetch the
leading magic, parse or cross-check the leading schema message, or inspect
the optional EOS marker; tail reads and coalescing may physically
over-read any unrequested bytes. Embedded batch statistics use the official
Arrow statistics value layout under the `JuliaArrow:batch_statistics.v1`
placement key (placement is scoped out of the upstream spec) and are
trusted for completeness: conservative lies cost pruning, narrow lies can
lose rows. Scan pushdown over duplicate column names is refused.
### C data and C stream interfaces
Every Core layout crosses the boundary: Boolean, integer, floating point,
null, decimal (32/64/128/256 in the `d:` form), date, time, timestamp (with
and without timezone), duration, all three interval units, UTF-8 and binary
(both offset widths), fixed-size binary, list, large list, fixed-size list,
struct, map, sparse and dense union (ids in the format string), dictionary,
Utf8View and BinaryView (`vu`/`vz`, with the C-Data-only trailing int64
buffer of variadic data-buffer lengths), ListView/LargeListView
(`+vl`/`+vL`), and run-end encoding (`+r`). Field metadata and schema-level
metadata cross both directions (schema metadata rides the stream's
struct-typed schema node; dictionary field metadata rides the wrapper node,
matching the C++ bridge, and import concatenates wrapper and dependent
pairs losslessly).
Foreign allocation extents cannot be verified by the ABI and are trusted
declarations; the producer must keep declared storage alive and unchanged
until Core releases it. Import checks the pointer tables, counts, descriptor
shape, and checked geometry that the ABI does expose. Import and export
apply the semantic validation tier — the same default as the IPC reader and
writer; `validate_full` is the caller's opt-in on either side. Field names
containing an embedded NUL are refused (C strings are NUL-terminated),
imported names must be valid UTF-8, and C strings longer than 1 MiB without
a terminator are refused instead of scanned. The format parser accepts only
the specified decimal integer grammar and bounds decimal descriptors and
union ids before recursive or geometry work. Empty offset layouts export one
non-NULL terminal zero offset for strict cross-implementation parity. The C
timestamp format has one empty-timezone spelling, so a Core empty string
canonicalizes to `nothing` when imported again.
Release callbacks use producer-owned canonical child and dictionary
topology, so cleanup does not depend on caller-mutated public counts or
pointer tables; they inspect canonical descendants' public release fields
to honor consumer moves. A callback transaction that fails before commit
restores its node to LIVE and returns at the void C boundary; a later
explicit call resumes it without repeating completed children. Callbacks
for one exported tree are serialized and legal only on Julia-attached
threads (they call Julia and take a `ReentrantLock`); there is no lock-free
foreign-thread trampoline. `reap!` performs an explicit registry scan;
there is no background reaper. Schema and array trees have independent
aggregate lifetimes and per-node control blocks.
`export_stream!` fills a caller-owned `ArrowArrayStream` that streams
batches as struct-typed arrays; each `get_schema`/`get_next` result is an
ordinary export root, producer-side failures surface through
`get_last_error` (EINVAL plus a NUL-terminated message owned by the stream
until replaced or released), and the stream's own root drops at its release
callback. `from_c_stream` moves a producer's stream, reads the schema once,
pulls batches whose trees each own one `ForeignOwner`, and surfaces
producer errors as exceptions carrying the producer's message. Stream
callbacks call into Julia, so they are legal only from Julia-attached
threads and calls on one stream must not overlap (the C stream spec itself
declares the structure not thread-safe).
The ABI layout gates include 32-bit expectations; only the 64-bit branch
is exercised (on the available hosts), the 32-bit branch is inspected.
### Facade
`Arrow.Table` materializes columns into plain Julia vectors (closed
fixed-width claims through Core's bulk typed path, everything else through
the dynamic accessors, then facade conversions: Dates types in both
directions, with sub-millisecond timestamps staying raw integers rather than
silently truncating). `Arrow.Stream` iterates record batches as one Table
each. `Arrow.write` accepts any Tables.jl source (partitions become record
batches), `DictEncode` marks a column for pooling, retained-schema rewrites
of a `Table`/`Stream` recursively preserve every descriptor that materialized
values can reconstruct, plus nullability and ordered metadata. Top-level
dictionary pools retain order, unused and duplicate entries, null entries,
and index width; multi-partition dictionary columns share one pool object.
Unregistered Union routing and nested dictionary pools are no longer present
after facade materialization, so those retained rewrites fail closed.
Registered ArrowTypes.jl public-domain values keep writer-side type evidence and can
reconstruct retained Union routing, including retained dense or sparse mode and
type IDs. Sparse children receive canonical hidden placeholders outside their
active rows. An abstract registered target accepts an extensionless concrete
subtype or one with the retained parent identity; a different explicit identity
fails closed. A nullable Dictionary<Null> with an unknown extension also fails
closed because materialized `missing` values cannot retain the difference
between a valid null-pool index and a null index. View buffer
topology, ListView overlap, and exact run segmentation rebuild canonically.
DataAPI metadata reads through. The facade applies ArrowTypes.jl lowering and
extension restoration recursively to top-level and nested values. There is no
lazy typed-view layer, no parallel writer pipeline, and no append-as-resume.
One column-scoped construction context caches `ArrowType` per Julia type,
extension shape and Union decomposition per Julia type, runtime Union branch
routes, and `JuliaType` per retained `Field`.
Tables column names are the explicit process-global `Symbol` boundary. Before
interning any novel top-level name, the facade preflights the complete schema:
4096 UTF-8 bytes per name, at most 65,536 novel names per table
materialization, and at most 1 MiB of novel-name bytes in total. Failure occurs
before partial interning. Nested Core names remain strings.
On the ordinary resolved paths, `toarrow` runs once for each value that reaches
lowering; dictionary categories are pooled before they are lowered. The
ArrowTypes 2.x fallback for an unresolved all-missing abstract storage type is
kept for compatibility. Recursive custom schemas and value containers, and
custom mapping nesting beyond 64 levels, fail with `ArgumentError`.
Fresh unresolved abstract declarations collect concrete subtype evidence once
for the complete column. This preserves subtype extensions and uses an explicit
bounded Union when the observed subtype Fields differ.
Declared writer Unions have at most 32 branches. Runtime writer or storage
inference has at most 8 distinct types across the complete column. This covers
abstract ArrowTypes storage, abstract or `Any` dictionary values, and abstract
retained ArrowTypes targets. The lower inferred limit bounds per-type trait and
candidate compilation; an explicit declared Union remains the schema authority
for wider intentional type sets.
Fixed-size-list storage signatures are exact through arity 1024. Larger
descriptors use compact `Tuple{Vararg{T}}` signatures so logical type resolution
cannot allocate in proportion to an untrusted list size. Extension Struct
signatures are exact through 1024 children only when child names are unique,
contain no embedded NUL, already exist as Julia `Symbol`s, are at most 4096
UTF-8 bytes each, and use at most 64 KiB in total. Otherwise the labelled Struct
remains unknown and materializes as ordered `Pair` storage. The bounded
ArrowTypes.jl Tuple compatibility exception may intern names only when the
complete child sequence is exactly `"1"`, `"2"`, …, `string(N)` for
`N ≤ 1024`; it can therefore add only `Symbol("1")` through `Symbol("1024")`.
Unknown extension labels return before preflight. Arbitrary or partly positional
Struct names never take this exception.
Unknown extension labels are probed with a non-interning Julia symbol lookup;
only an existing symbol can reach `JuliaType(Val(...))`. Unsupported-extension
warnings are deduplicated by the complete label. Each table materialization
emits at most one warning for each of 16 distinct labels, then one suppression
notice for further distinct labels. A warning displays at most 128 UTF-8 bytes
of its label. The built-in `JuliaLang.Symbol` mapping likewise rejects a storage
string that is not already interned, rather than growing process-global symbol
state from input.
Writer-side `ArrowType` results are checked at the trait cache boundary. A
returned concrete tuple above arity 1024 is rejected before downstream writer
specialization, including the default mapping for a tuple value. A custom trait
method itself is trusted Julia code.
Hidden retained composite data is built from Field plus length. Null-only
fixed-size lists recurse without per-slot Julia placeholders, including wholly
or partly inactive sparse-Union children.
Retained descriptor matching recognizes only storage families whose builders
can enforce the original schema exactly. These include sequence layouts,
opaque binary and wide-decimal byte storage, interval NamedTuple storage, and
compatible temporal units. The retained builders enforce fixed widths, list
sizes, interval shapes, exact temporal conversion, and sorted Map claims.
Concrete declared element types remain planning evidence when a column is empty
or contains only missing values.
## Trim-compile support (JuliaC `--trim=safe`)
`test/trim_compile_tests.jl` compiles `test/trim_entrypoint.jl` with
JuliaC's `--trim=safe` and requires **zero verifier errors, zero verifier
warnings, and a produced binary that runs to exit 0**. The workload covers
regions, mmap, C-data export/import/release, dynamic values, typed values
(a `from_c_data` → `materialize(Int64, …)` scenario among them), and
validation errors. It also covers ArrowStrings construction, inline and view
access, missing values, comparison, and materialization. The rules that keep a
runtime-tagged core there:
- **Closed-set dispatch ladders.** Dispatch on an abstract-typed field is
dynamic; the descriptor set is closed (it IS the layout registry), so
`@inline` `isa` ladders (`layoutspec_of`, `_value_of`, `_materialize_of`,
`typeequal`, `descriptorname`, `_validate_descriptor_of`, and the C-data
`formatstring_of`) devirtualize every generic entry point. Multiple
dispatch stays the per-layout extension surface underneath. Plain
forwards do not work: the verifier reports the abstract call site as
unresolved rather than enumerating the closed method table.
- **Narrow after `||`-checks.** An `isa` test inside an `||` condition does
not narrow the binding; a typeassert after it (`rt::IntType`) is what lets
`primwidth`/`_load_int` resolve.
- **Literal load widths.** `loadat(b, T, off)` with a runtime `T::DataType`
leaves the raw-load path unresolved; accessors branch to literal widths.
- **A recursion cycle's non-inlined edge is a compiled function with
all-concrete argument types.** Dynamic recursion routes through the public
`getvalue(Field, ArrayData, Int64)`; the typed path splits its edge into
an `@inline` scalar fast ladder (scalar children SROA into the parent
loop) plus a compiled shell for composites, with `::T` asserts pinning
claim-typed returns. `@generated` struct rows keep every field's claim a
literal type past the arity-4 `ntuple` cliff.
- **CAS, not `Core.modifyfield!`.** The verifier has not implemented the
read-modify-write builtin (each `@atomic x.f += 1` is a warning), while
`@atomicreplace` verifies clean.
- **`Ptr{Cvoid}` finalizers and cfunctions.** Base's generic
`finalizer(f, o)` is `@nospecialize`d and unresolvable; the typed pointer
form (`finalizer(@cfunction(...), o)`) is an ordinary ccall. Release
actions are runtime Ptr-ABI cfunctions (an `Any`-argument cfunction is
rejected), never stored in module-level `const`s (raw-pointer consts are
precompile-poison).
- **Concrete containers at the boundary.** Struct scalars are
`Vector{Pair{String,Any}}` on the dynamic path; lists materialize as
`Vector{Any}` without a runtime-narrowing comprehension.
- **Kwcall discipline.** A `Union`-typed keyword argument makes the kwcall
tuple imprecise — branch on presence instead; abstract-typed keyword calls
need positional twins; boxed closure captures (reassigned-under-`try`
locals) are rejected — single-assign before `try`.
- **Beware splatting Base conveniences.** `write(filename, x)` and
`open(...) do` route through vararg-splatting internals; `mktempdir`'s
cleanup registry parks the trimmed runtime's scheduler.
- Heterogeneous NamedTuple ingestion (`batch(nt)`, `fromjulia_struct`) is
runtime-schema builder work outside the trim-safe surface.
## Compression
Both IPC directions implement spec buffer compression for **LZ4_FRAME and
ZSTD** through the direct CodecLz4/CodecZstd dependencies over
TranscodingStreams. Each reader lazily creates raw native codec contexts and
closes them on every `readstream` exit path; each writer owns one lazily
initialized compressor per codec and finalizes it on every writer exit
path; there are no global pools. The write side emits the Int64
uncompressed-length prefix per buffer and stores incompressible payloads raw
behind the `-1` sentinel. The read side checks the prefix and the sentinel;
a zero-byte wire buffer may omit the prefix; a nonzero compressed buffer,
including declared length zero, must contain a valid frame. Declared sizes
are bounded and charged to the shared reader budget before one exact-sized
output vector is allocated; the codecs decode directly from the wire slice
(its region rooted across the native call with `GC.@preserve`) with no
payload copy and no growable output; the LZ4 loop requires one complete
frame, exact input consumption, and exact output size, and the ZSTD
one-shot decode uses the same exact destination.