Analytics, columnar formats, and bridges
The columnar path
A table is a CX element, so everything we promise for a
document holds for a billion rows. The same brackets that carry a
configuration file carry a typed columnar table; the same canonical
bytes and address that anchor one document anchor a dataset; the
same evaluator that renders a report streams over a file larger
than memory. Our columnar path has three load-bearing pieces —
a logical shape, a physical wire form, and bridges to the
columnar ecosystem — and this section is how they compose. Every
example on it is a case of the data ring's table suite, replayed
against this guide's cx when the page was built.
The three pieces are: a logical data shape (the [table[…]]
block, with typed columns and nullable cells); a physical wire
form (CXCol, the column-oriented streaming binary form,
tag 0x63 in spec/03-approved/core/data-bin.md §3.11);
and bridges (the Arrow C Data Interface for zero-copy handoff
in process, the Arrow IPC stream for transport, Parquet for
cross-tool storage).
- Logical:
[orders [table[id::int customer::string]] …]— see table-shape. Authored in CX text, parsed into the same tree as any other element, validated against.cxsschemas, addressed by SHA-256 over canonical bytes. - Physical: CXCol — row-group chunked, optionally zstd-compressed per chunk, optionally schema-driven for density. See cxcol.
- Bridges: the Arrow stream out of CXCol, and Parquet through it. See arrow-bridge and parquet-bridge.
- Streaming: the streaming evaluator
(
spec/03-approved/core/streaming.md) emitsStartTable/RowGroup/EndTableevents over CXCol input, so a[?for]or a[?modify]walks a table that does not fit in memory one row group at a time. - Identity: a CXCol file has one SHA-256 over its canonical (uncompressed) bytes. The compression level does not move it; the chunk boundaries do — see cxcol-identity.
The normative documents are
spec/03-approved/core/data-bin.md
§3.10-3.13 (the wire form) and
spec/03-approved/misc/table-api.md
(the table surface every binding exposes); this section is the
working summary.
Naming. CXCol is the column-oriented streaming format. The
CXDB namespace is reserved for a future query engine and does
not name the file format. .cxbin is the file extension
(.cxcol is a deprecated alias, read-only —
spec/03-approved/process/governance.md
§12.2); the cx_table_* prefix is the C ABI streaming family
(spec/03-approved/core/abi.md §2.10).
The [table[…]] data shape
The [table[…]] clause-child opener declares a typed columnar
table at the logical layer. It is a CX element like any other —
same brackets, same parse rules, same canonical-bytes contract —
and the parser recognizes the glued [table[ opener (grammar
[29]) and produces a table value rather than a generic element.
The element's own name is the table's name; the round trip through
the data reading is byte-exact:
input.cx
[users [table[name::string age::int active::bool]]
alice 30 true
bob 25 false
]
$ cx --from=cx --to=cx input.cx
[users [table[name::string age::int active::bool]]
alice 30 true
bob 25 false
]
Fixture tab-001-typed-columns in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
The [table[…]] clause carries the column spec: whitespace-
separated name::type entries, where an entry without a type
declares a string column. The body is a flat run of
whitespace-separated cells, partitioned into rows by the column
count (grammar [29c] — each row supplies exactly one cell per
column). A typed column's cells are typed on read, as any scalar
is:
input.cx
[t [table[n::int]]
1
2
3
]
$ cx --from=cx --to=cx input.cx
[t [table[n::int]]
1
2
3
]
Fixture tab-005-typed-int-cells in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
A cell that cannot carry its declared column type is refused
loudly rather than coerced — the encode-refusal rule of
spec/03-approved/core/data-bin.md
§3.10.3, never wrap, never widen, never approximate — and so is a
column whose declared type and cell annotation disagree (the table
suite's tab-026 pins that refusal; its recorded diagnostic is a
message rather than a code, which is why this page names it
instead of replaying it).
Supported column types match the scalar type system:
int and the sized integers (i8–i64, u8–u64),
bigint, float and the reduced widths (f32, f16),
decimal, bool, string, bytes, date, and datetime.
The full catalog with byte encodings is in
spec/03-approved/core/data-bin.md
§3.10.3 + spec/03-approved/misc/type-mapping.md.
Null cells
Any column can carry null cells. A null cell is written as the
literal null — a bare token in cell position; an empty cell is
not allowed in the wire form:
input.cx
[t [table[a b]]
alice null
bob 30
]
$ cx --from=cx --to=cx input.cx
[t [table[a b]]
alice null
bob 30
]
Fixture tab-009-null-cell in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
On the wire, a column with null cells uses the nullable wrapper
of spec/03-approved/core/data-bin.md
§3.10.5: type code 0x80, then the inner type code, then a null
bitmap of ceil(rows/8) bytes (bit set = null), then the packed
payload of the non-null cells only. Each binding's table surface
answers a null cell with its host's own "no value" form.
Collection cells
A cell is a value, so a cell may be a collection — an array or a map literal — and the table stays a table:
input.cx
[t [table[name tags]]
alice [admin, user]
bob [user]
]
$ cx --from=cx --to=cx input.cx
[t [table[name tags]]
alice ['admin', 'user']
bob ['user']
]
Fixture tab-012-array-cells in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
input.cx
[t [table[name attrs]]
alice {role: admin, age: 30}
bob {role: user, age: 25}
]
$ cx --from=cx --to=cx input.cx
[t [table[name attrs]]
alice {role: admin, age: 30}
bob {role: user, age: 25}
]
Fixture tab-013-map-cells in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
A table may also nest inside an element like any child, and an
element that happens to be named table composes as an ordinary
element — only the glued opener declares a table:
input.cx
[wrap [users [table[name age]] Ada 36]]
$ cx --from=cx --to=cx input.cx
[wrap
[users [table[name age]]
Ada 36
]
]
Fixture tab-027-nested-one-deep in conformance/../deps/cx-core-data/conformance/table.cxd — replayed against this cx when the page was built.
Dictionary encoding
Dictionary encoding is an opt-in container form — tag
0x62 in spec/03-approved/core/data-bin.md
§3.10.2. Each column's payload starts with a marker: plain
(the §3.10.3 layout) or dictionary-encoded (distinct values
in insertion order, then one varint index per row). Decoders
rebuild the values transparently; no table surface exposes the
indices.
Canonical writers emit the plain 0x60 form. A writer that opts
into 0x62 uses a dictionary for a column only when the
dictionary actually saves bytes — the canonical constraint of
§3.10.2, evaluated per column — so categorical data (status,
region, type) dictionary-encodes and high-cardinality data stays
plain. CXPath = and [?match] compare the decoded value, so a
program is dictionary-agnostic.
Schema validation
Tables validate against .cxs schemas the way ordinary elements
do — the table block is an element, and the schema language of
spec/03-approved/core/schema.md
applies to it as to any other. See [[data#schema]] for the
language. At the binary layer, a CXCol file can carry a schema
reference that names its schema by content hash (see
cxcol-schema-driven).
The table surface of a binding
Every binding exposes a table value for [table[…]] blocks —
one surface, ratified in
spec/03-approved/misc/table-api.md
§3, spelled in each host's own naming: the cols / types /
row_count / col_count properties; the row / column /
col_at / cell / cell_by_name accessors; the slice /
head / tail / select_cols projections; row iteration; and
the to_cx / to_csv / to_json / to_data_bin /
to_dict_list conversions. The four shipping bindings are on
bindings; what a table method
answers is what the corpus cases above answer, because the
surface is a projection of the same value.
CXCol — column-oriented streaming format
CXCol is the physical wire form for tabular data: column-oriented,
row-group-chunked, content-addressable, and streamable in bounded
memory. The data-bin tag is 0x63; the file extension is
.cxbin. The normative byte-level layout is in
spec/03-approved/core/data-bin.md §3.11,
and the data ring's corpus grades it in four families —
data_bin_chunked, data_bin_compression,
data_bin_schema_driven and data_bin_arrow — each case a CX
document beside the exact bytes it must encode to. Those cases
record bytes, not text, so this page names them rather than
replaying them.
The format is a single sequential pass: header, column spec, then a stream of row groups, then a terminator. A writer emits one row group at a time and produces a billion-row table in memory bounded by the largest row group; a reader pulls row groups one at a time and skips a group by its length prefix without decoding it.
Wire layout
A CXCol stream is:
- 12-byte header. Magic
CXCol(5 bytes,0x43 0x58 0x43 0x6F 0x6C), version byte (0x01), flags byte,max_depth(uint32 LE), reserved byte. Flag bit 0 is endianness (1 = little-endian); flag bit 1 signals schema-driven mode (see cxcol-schema-driven); bits 2–7 are reserved and must be zero. - Column spec. The chunked-table tag
0x63, thenuvarint(col_count), then one col-spec per column — the column name as a tagged string plus its type code (the §3.10.1 shape). Declared once at the head — every row group in the file conforms to this spec. - Row-group sequence. Each group is
uvarint(body_byte_len) <body-tag> <body>.body-tag0x01= plain (uvarint(row_count)then column-major buffers);0x90= the zstd page-compression wrapper (§3.12), whose decompressed bytes form the same plain body. - Terminator. A single
0x00byte (a zero-length row group) closes the stream.
2²⁰ (1,048,576) rows per group is the canonical chunk size —
cx canonical and cx hash re-chunk a CXCol input to it, and a
writer that wants cross-system hash invariance uses it. A
different chunk size produces a valid CXCol file with a different
SHA-256.
Content-addressable identity
A CXCol file's SHA-256 is computed over its canonical
uncompressed bytes — the bytes you would see if every 0x90
page-compression wrapper were unwrapped to its 0x01 plain
form. Compression is a transport and storage choice; identity is
a content choice; the format keeps them orthogonal, and the
data_bin_compression cases pin that a compressed and a plain
encoding of one table share one address.
- Same data + same chunking + same schema → same hash, whatever the zstd level, and whether compression was applied at all.
- Different chunk size → different hash. Chunk boundaries are part of the canonical form; the 2²⁰-rows-per-group convention is the cross-writer rule.
- The hash composes with hash. The same SHA-256 primitive that addresses a CX document addresses a CXCol file — caching, dedup, signing and content-addressed storage treat both the same.
Streaming reader / writer
The streaming surface is the C ABI reader / writer family in
spec/03-approved/core/abi.md §2.10
(capability bit 21): cx_table_reader_open /
cx_table_reader_open_fd / cx_table_reader_schema /
cx_table_reader_next / cx_table_reader_close, and
cx_table_writer_open / cx_table_writer_open_fd /
cx_table_writer_emit_row_group /
cx_table_writer_close_get_bytes / cx_table_writer_close.
A reader pulls one row group at a time; a writer pushes one at a
time and closes to the framed bytes. Each binding wraps the
family in its host's idiom — a context-managed reader and
writer where the host has them, the raw symbols where it does
not — and bindings says which binding ships which.
The memory budget is one row group in flight per stage. A reader chained into a CXPath filter chained into a Parquet writer holds three row groups at once, whatever the table's logical row count. See streaming for the directive-level view.
Schema-driven mode
When both sides of the wire hold the schema, header flag bit 1
enables schema-driven mode: per-value type tags are omitted for
the values the schema declares, and reader and writer walk the
schema in lockstep with the data. Wire density approaches
Parquet's typed encoding; the data_bin_schema_driven cases pin
the bytes, and a closed-mode schema refuses a column it did not
declare.
Schema-driven mode is opt-in — cx_to_data_bin --schema-driven
at the ABI, the corresponding call in each binding — and a
schema in scope never silently promotes the encoder. The schema
reference forms (0x10 content-hash, 0x11 inline, 0x12
content-hash + name hint, 0x13 self-describing multihash) are
spec/03-approved/core/data-bin.md §3.13.1;
the cx table subcommand resolves --schema-from= to a content
hash before it emits.
Schema-driven mode needs the reader to retrieve the schema by its content hash. Local lookup is the default; the callback-shaped content-store ABI is a planned follow-up.
Arrow bridge — zero-copy interop
CXCol bridges to Apache Arrow through the Arrow C Data
Interface — a struct-based ABI under which two libraries share
columnar buffers without copying them. The CX side is
libcx_arrow, a separate dynamic library over libcx and the Arrow
C-Data ABI; core libcx is Arrow-free, and capability bit 23
advertises Arrow when libcx_arrow is loaded. The
data_bin_arrow cases of the data ring pin the export and import
byte for byte.
The bridge exports four C ABI symbols
(spec/03-approved/core/abi.md §2.11):
cx_arrow_export_open / cx_arrow_export_open_fd for
CXCol → Arrow, and cx_arrow_import_to_data_bin /
cx_arrow_import_to_data_bin_fd for Arrow → CXCol. A binding
wraps them as an export (framed CXCol bytes in, an Arrow record
batch reader out) and an import (the inverse) — the per-binding
Arrow libraries are tabulated in §2.11.
Zero-copy handoff
The C Data Interface uses two structs: ArrowSchema (column
types and names) and ArrowArray (per-batch buffers — null
bitmap, offsets, values). CX points these at its own column
buffers; an Arrow-native consumer reads from the same memory
pages CX wrote.
Ownership crosses the boundary explicitly. The exporter fills an
ArrowArrayStream with a release callback; the consumer calls
release when it is done; CX's callback frees the underlying
CXCol buffers. There is no garbage-collector interplay — a
binding holds the stream as an opaque handle until it is
finished with it.
Type mapping
The CX ↔ Arrow type map is normative
(spec/03-approved/misc/type-mapping.md).
In summary:
int/i64↔Int64;i32↔Int32; the sized variants (i8–u64) map to the corresponding Arrow sized integer types.float/f64↔Float64;f32↔Float32;f16↔Float16.bool↔Boolean(bit-packed in Arrow).string↔Utf8(orLargeUtf8for a column whose total byte length exceeds 2 GB).bytes↔Binary(orLargeBinarysimilarly).date↔Date32(days since 1970-01-01, proleptic Gregorian).datetime↔Timestamp(Nanoseconds, UTC)(Arrow format stringtsn:UTC).decimal↔Decimal128/Decimal256by coefficient width.- A nullable column → Arrow's nullable variant (validity bitmap).
Column-type coverage in libcx_arrow is all ten column
types — int / i8 / i16 / i32 / float / bool / string / date /
bytes / datetime (the last as timestamp[ns, UTC]). Deferred:
decimal and dictionary-encoded columns — each answers a clear
not yet supported error rather than a lossy fallback.
Arrow IPC stream format
Beyond in-process handoff, CXCol emits Arrow IPC streams for
cross-process or cross-host transport. The IPC stream is Arrow's
own serialization (schema header + record batches + EOF),
distinct from the C Data Interface (in-memory structs). The
cx table dump FILE --to=arrow [--output=FILE] subcommand
(spec/03-approved/misc/cli.md)
writes it, and each binding's Arrow module reads and writes it
as bytes or as a file.
Arrow Flight (planned)
Arrow Flight is Arrow's gRPC-based RPC framing for cross-process record batches — the natural next layer above the IPC stream: a Flight server publishes batches by descriptor; a client subscribes. CX integration is planned: no Flight surface ships, and the tooling-layer story will be specified when it merges.
For cross-process CXCol transport today, carry Arrow IPC over
your existing transport (HTTP, a pipe, a socket); the
[?http-client] directive carries an IPC stream as a binary
body.
Parquet bridge
CXCol bridges to Apache Parquet through Arrow: CXCol → Arrow stream
→ Parquet file. Core libcx has no Parquet C++ dependency —
adding one would couple every binding to a heavyweight build-time
toolchain — so the Parquet adapter lives at the binding layer,
chained off the Arrow bridge.
spec/03-approved/misc/table-api.md
§3 declares the per-binding adapter slot; the PyPI binding ships
the packaged one-call adapter, and the other bindings reach
Parquet through their host's Arrow library today.
Writing Parquet
The adapter walks framed CXCol bytes → Arrow record batches →
the host's Parquet writer; the writer is the host's choice, and
the CXCol side provides the Arrow stream. Defaults are the
Parquet ecosystem's (Snappy, not zstd), because the output is
for Parquet readers that assume Snappy; zstd is CXCol's own
default for its 0x90 page compression. Match the row-group
size to CXCol's canonical chunk (2²⁰ rows) and the two formats
chunk alike.
Reading Parquet
The inverse: the host's Parquet reader produces Arrow record batches, and the bridge imports them as framed CXCol chunked-table bytes — ready for a whole-value read or a streaming reader pass. Column projection and predicate pushdown are the host Parquet reader's work, done before the batches reach CX: a reader given per-row-group statistics skips the groups a predicate rules out, so the I/O saved is proportional to the groups skipped; a file without statistics still reads correctly as a full scan.
CX ↔ Parquet type mapping
The mapping is normative. Most types round-trip cleanly through Arrow; a few CX types have lossy edges, and the bridge reports them loudly rather than coercing silently.
int/i64→INT64.i32→INT32;i16/i8→INT32withINT(N, true)logical type; unsigned similarly.f64/float→DOUBLE;f32→FLOAT;f16→FIXED_LEN_BYTE_ARRAY(2)/FLOAT16(the latter non-standard before Parquet 1.13).bool→BOOLEAN.string→BYTE_ARRAYwith theSTRINGlogical type (UTF-8).bytes→BYTE_ARRAY.date→INT32with theDATElogical type.datetime(UTC) →INT64withTIMESTAMP(NANOS, isAdjustedToUTC=true).decimal→DECIMAL(p, s)(Parquet caps precision at 256 bits).- A nullable column →
OPTIONALrepetition.
A type outside the bridge's coverage (see arrow-type-mapping) refuses with a clear error at the Arrow boundary rather than degrading — the same loud-never-lossy posture as the wire form itself.
Round-trip identity
CX → Parquet → CX is not in general byte-identical. Parquet is a presentation-layer format: footer offsets shift, row-group sizes are writer-dependent, encoding choices (dictionary thresholds, page sizes, the codec) are per writer; many byte-level Parquet files encode the same logical data. CXCol fills exactly that niche — the hashable origin; Parquet is the bridge.
Logical round trip is normative: a CX table emitted as
Parquet and re-imported produces the same [table[…]] block
with the same column names, types, row count and cell values
(modulo the documented lossy edges of 5.5.3) — which is the
byte-exact CX round trip the table suite grades, taken through
one more format.
Schema identity across the bridge
Parquet output carries the data, not the CX schema reference.
Schema identity lives at the CXCol layer: a schema-driven file
names its schema by content hash (the §3.13.1 reference forms),
and cx table resolves --schema-from= to a content hash before
it emits
(spec/03-approved/core/data-bin.md
§3.13.1). A Parquet round trip re-enters CX as a self-describing
table; re-attach the schema by validating, or by re-encoding
schema-driven against the same schema hash.
Streaming evaluator over columnar data
The streaming evaluator
(spec/03-approved/core/streaming.md)
walks CXCol chunks one at a time. The normative throughput gate is
≥ 200 MB/s sustained per streaming shape
(spec/03-approved/core/code.md
§11.4.4, gate 15), re-measured by make bench-code-streaming; the
gate register carries its last honest verdict, and we state it
there rather than in prose that goes stale.
The evaluator emits three table-specific events when it meets a CXCol input: StartTable (the column spec), RowGroup (one chunk's data), EndTable (close). A directive that consumes the stream sees one row group at a time and never materializes the whole table.
The consumer surface for a CXCol stream today is the CLI and the
table surface of each binding (cx table info / dump / load,
and a binding's table methods):
cx table info big.cxbin [# column spec, row count, size #]
cx table dump big.cxbin --to=cx [# stream to CX text #]
The design direction, not yet shipped as a CX-code surface: any
directive that takes a CXPath iterator participates in a CXCol
stream — [?for] / [?modify] / [?map] / [?reduce] (see
code). Bounded memory holds end to end: a pipeline of N stages
holds N row groups in flight, not N × the row count.
Streaming events
The event vocabulary
(spec/03-approved/core/streaming.md)
extends the standard StartElement / Scalar / EndElement
events with three table-specific variants.
- StartTable — fields:
name(string),col_spec(binary blob; the column schema in ast_bin form). Emitted in place of theStartElementthat would otherwise wrap a[table[…]]body. - RowGroup — fields:
row_count(u32),payload(binary blob; one row group's column buffers). One event per row group, in source order. - EndTable — fields:
name(string). Closes the sequence in place of the correspondingEndElement.
A non-chunked 0x60 (whole-table) input emits the existing
StartElement / per-cell-Scalar / EndElement sequence
unchanged; only a 0x63 chunked input produces the new events.
A consumer that does not care flattens each RowGroup into
StartElement / row / EndElement triples with a helper.
Performance posture
The gate protocol is fixed by
spec/03-approved/core/code.md
§11.4.4: every measured streaming shape must sustain a mean
≥ 200 MB/s with no single trial below 80 % of the mean, and must
demonstrably stream (more than one output chunk, the
implementation's own streaming predicate reporting true). Re-run
with make bench-code-streaming; the register row is the
verdict.
On the write side, the ABI streaming writer compresses a row
group only when its body exceeds 64 KiB and compression beats
0.85 of the original size
(spec/03-approved/core/abi.md §2.10).
Ecosystem integrations
The Arrow C Data Interface is the integration layer the analytical ecosystem standardized on. Once CX exposes Arrow, the rest of the stack lights up — DuckDB, Polars, DataFusion, pandas, Spark by way of Parquet — most with zero-copy handoff in process, the rest with one copy at the boundary. Each integration below is the Arrow bridge of arrow-bridge plus the consumer's own Arrow entry point; the binding you use supplies the glue in its host's idiom, and bindings says which binding packages which.
DuckDB
DuckDB consumes an Arrow table registered on a connection — zero-copy from CXCol's column buffers into DuckDB's vectorized executor through the C Data Interface. Export the framed CXCol bytes, register the Arrow table, run SQL; DuckDB's executor reads CX's column buffers during the scan, the result set is materialized in DuckDB's own memory, and the bridge's import brings a result back as framed CXCol bytes — the symmetric inverse.
Polars
Polars is Arrow-native at the buffer level: CX → Polars goes
through the same export call — zero-copy for the underlying
buffers — and a Polars query runs on the frame from there. In
the Rust binding the same handoff runs over the C ABI: the
cx_arrow_export_open stream feeds the arrow crate's
ffi_stream reader, and Polars consumes the record batches; a
packaged convenience wrapper there has not shipped.
pandas
pandas is not zero-copy at the buffer level — its column
model is NumPy arrays with type-system mismatches against Arrow
(no nullable integers, no first-class strings without
StringDtype). CX → pandas is one Arrow round trip plus one
pandas conversion: expect one full data copy at the boundary.
spec/03-approved/misc/table-api.md
§3 declares a to_pandas() adapter slot on the table type that
no binding has implemented yet; the explicit Arrow route is the
shipped path.
Where zero copy matters, prefer Polars over pandas. pandas stays supported because the data-science ecosystem assumes it, but the cross-boundary cost is real on large tables.
DataFusion (planned)
Apache DataFusion is a Rust query engine over Arrow. Its
TableProvider trait accepts arbitrary Arrow record-batch
sources, so CXCol is a natural fit: register a .cxbin file as
a DataFusion table and run SQL or DataFrame queries with the
same zero-copy guarantees as Polars.
Status: planned — no packaged CX-side convenience layer
ships. An adopter who needs DataFusion today wires it by hand:
cx_arrow_export_open over the C ABI into the arrow crate's
ffi_stream reader, and the record batches into a DataFusion
MemTable. The path works; it lacks the convenience layer.
Spark and cluster compute
Spark reads and writes Parquet natively, so CXCol's Parquet
bridge (see parquet-bridge) is the integration path: emit
.parquet from CX, point Spark at the directory, run Spark SQL.
For zero copy in cluster contexts, Arrow Flight is the long-term
path; status is planned per arrow-flight.
The Parquet bridge is one-way at this layer — Spark's writes
come back through cx table load output.parquet --to=cx. For a
round-trip workflow, keep the CXCol file as the canonical origin
and treat Spark's outputs as derived artifacts.
Performance characteristics
The columnar path's budgets and where each is measured. The spec fixes the gates; the bench targets re-measure them; the numbers live where they are re-run, not in prose that goes stale.
- Streaming throughput. Spec gate: mean ≥ 200 MB/s per
streaming shape, no trial below 80 % of the mean
(
spec/03-approved/core/code.md§11.4.4, gate 15; bench:make bench-code-streaming); the gate register carries the last honest verdict. - Parquet write (default Snappy, 2²⁰ row group). Measured by the host's Parquet writer on the CXCol → Arrow → Parquet path; throughput is dominated by the Parquet codec, not the CXCol bridge. Profile against your chosen writer; no normative CX-side gate.
- Arrow zero-copy handoff. O(metadata) per batch — the schema and the per-column buffer pointers are populated, no buffer is copied. Time per export is independent of row count and proportional to column count.
- Predicate pushdown. When the Parquet writer emitted per-row-group statistics, the host Parquet reader skips the groups a predicate rules out — I/O saved is proportional to the groups skipped; a file without statistics degrades to a full scan.
- Memory budget per stream stage. One row group in flight.
The streaming evaluator
(
spec/03-approved/core/streaming.md) is bounded bymax(row_group_byte_size)whatever the table size.
The 200 MB/s gate is for the streaming evaluator, not the raw CXCol reader — the reader has no evaluator pipeline in front of it, and its throughput is not separately gated or published. Per binding, adapter throughput varies by host; the pandas route is the slowest because of its boundary copy.
When to use CXCol vs Parquet vs Arrow vs JSON
Pick by use case. The four formats overlap on the columnar path, and each has a niche the others fill badly.
- CXCol — when content-addressable identity matters (caching, dedup, signing, provenance) or when you want streaming with the CX type system end to end. Single-writer, sequential-decode, hashable. A single-file format, not a query engine (take the Arrow bridge into DuckDB / Polars / DataFusion for that).
- Parquet — for cross-ecosystem interop (Spark, BigQuery, Snowflake, Trino, Athena, the Hadoop-lineage tools). Carries the data, not CX's schema identity or metadata (see parquet-metadata). Not hashable in the byte-stable sense — different writers produce different bytes for the same logical data.
- Arrow IPC stream — for in-process or LAN-local zero copy between CX and another Arrow-native consumer. No compression by default (Arrow is layout-oriented); not designed for long-term storage.
- JSON / JSONL — for ad-hoc human-readable interop or a consumer that truly is JSON-shaped. Lossy on type metadata (int and float kinds merge, date-times become strings, dictionary encoding is gone). The CX → JSON projection (see [[surfaces#json]]) handles the shape; for tabular data, JSONL (one row per line) is the conventional wire form.
The decision tree, compressed:
- Need a hashable, content-addressable origin? → CXCol.
- Sharing with Spark / Snowflake / BigQuery / other non-Arrow-native tools? → Parquet.
- In-process handoff to DuckDB / Polars / DataFusion / pandas? → Arrow (with CXCol as the origin).
- Debugging, ad-hoc inspection, or a JSON-shaped consumer? → JSON / JSONL.
In practice the layering is: CXCol as canonical origin → Arrow as the in-process bridge → Parquet for cross-ecosystem export. JSON is the human-readable view. The same logical table moves through all four; identity stays at CXCol.
See also
- identity — canonical bytes + SHA-256. CXCol identity rests on the same primitive over canonical bytes; the rule extends to chunked tables and schema-driven encoding.
- surfaces — the JSON / YAML / TOML / XML / Markdown projections. CXCol is the analytical surface; the others are the interchange surfaces.
- bindings — the four shipping bindings, each with the table surface and the streaming reader / writer in its host's idiom.
- [[code#streaming]] — the directive-level streaming view:
[?for]/[?modify]over CXCol inputs in bounded memory. spec/03-approved/misc/table-api.md— the typed columnar table surface ratified across the four bindings.spec/03-approved/core/data-bin.md§3.10-3.13 — chunked-table wire format, page-compression wrapper, schema-driven mode.spec/03-approved/core/streaming.md— the streaming evaluator and the StartTable / RowGroup / EndTable event vocabulary.