CX · Reference guideAnalytics, columnar formats, and bridges
PlaygroundDownloadsAboutv0.18.0-pre.1

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 .cxs schemas, 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) emits StartTable / RowGroup / EndTable events 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, then uvarint(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-tag 0x01 = 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 0x00 byte (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 (or LargeUtf8 for a column whose total byte length exceeds 2 GB).
  • bytes ↔ Binary (or LargeBinary similarly).
  • date ↔ Date32 (days since 1970-01-01, proleptic Gregorian).
  • datetime ↔ Timestamp(Nanoseconds, UTC) (Arrow format string tsn:UTC).
  • decimal ↔ Decimal128 / Decimal256 by 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 → INT32 with INT(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_ARRAY with the STRING logical type (UTF-8).
  • bytes → BYTE_ARRAY.
  • date → INT32 with the DATE logical type.
  • datetime (UTC) → INT64 with TIMESTAMP(NANOS, isAdjustedToUTC=true).
  • decimal → DECIMAL(p, s) (Parquet caps precision at 256 bits).
  • A nullable column → OPTIONAL repetition.

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 the StartElement that 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 corresponding EndElement.

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 by max(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.