This directory contains examples demonstrating how to use the Zerobus C++ SDK to ingest data into Databricks Delta tables.
- Overview
- JSON Examples
- Protocol Buffers Examples
- Arrow Flight Examples (Beta)
- Prerequisites
- Building the Examples
- Common Code Patterns
- Single-Record vs Batch Ingestion
- Choosing JSON vs Protocol Buffers
- Troubleshooting
The SDK supports two record wire formats on a standard Stream, plus a separate
columnar ArrowStream:
Serialization Formats:
- JSON — Simpler, no schema generation required. Great for getting started.
- Protocol Buffers — Type-safe binary encoding. The
examples build the descriptor at runtime from Unity Catalog metadata
(
ProtoSchema::from_uc_json), so no.protofile orprotocis required. - Arrow Flight (Beta) — Columnar Arrow
RecordBatchingestion over the Arrow Flight protocol.
Beyond the basic ingestion flow, several examples also demonstrate advanced
features inline: an async ack callback and a custom HeadersProvider in
json/batch.cpp, recovering unacknowledged records after a
failure in json/single.cpp, and Arrow IPC compression in
arrow/arrow_ingest.cpp.
Ingestion Methods:
- Single-record (
ingest_json_record/ingest_proto_record) — ingest records one at a time. - Batch (
ingest_json_records/ingest_proto_records) — ingest multiple records at once with all-or-nothing semantics. Preferred in hot paths: it amortizes the per-call FFI crossing. - Arrow batch (
ingest_batch) — ingest one ArrowRecordBatch(one or many rows) over Arrow Flight.
Available Examples:
| Example | Format | Method | Binary |
|---|---|---|---|
| JSON Single | JSON | Single-record | json_single |
| JSON Batch | JSON | Batch | json_batch |
| Proto Single | Protocol Buffers | Single-record | proto_single |
| Proto Batch | Protocol Buffers | Batch | proto_batch |
| Arrow | Arrow Flight (Beta) | RecordBatch |
arrow_ingest |
Create a table in your Databricks workspace:
CREATE TABLE catalog.schema.orders (
id INT,
customer_name STRING,
product_name STRING,
quantity INT,
price DOUBLE,
status STRING,
created_at TIMESTAMP,
updated_at TIMESTAMP
);Replace catalog.schema.orders with your actual catalog, schema, and table name.
- In your Databricks workspace, go to Settings > Identity and Access.
- Create a service principal or use an existing one.
- Generate OAuth credentials (client ID and secret).
- Grant the service principal these permissions on your table:
SELECT— read table schemaMODIFY— write data to the tableUSE CATALOGandUSE SCHEMA— access catalog and schema
Every example reads its connection settings from the environment, so no value is ever baked into source. Export these five variables before running any example (the same names the Go, TypeScript, Java, and Python SDK examples use):
export ZEROBUS_SERVER_ENDPOINT="https://<your-shard-id>.zerobus.<region>.cloud.databricks.com"
export DATABRICKS_WORKSPACE_URL="https://<your-workspace>.cloud.databricks.com"
export ZEROBUS_TABLE_NAME="catalog.schema.orders"
export DATABRICKS_CLIENT_ID="<your_databricks_client_id>"
export DATABRICKS_CLIENT_SECRET="<your_databricks_client_secret>"For Azure, use .azuredatabricks.net hosts in the endpoint and workspace URL.
The proto examples additionally read the Unity Catalog table metadata JSON from the environment (the JSON examples don't). See the Protocol Buffers README for why it's needed and the full two-step fetch:
DATABRICKS_TOKEN="$(curl -sS --fail --request POST \
--user "$DATABRICKS_CLIENT_ID:$DATABRICKS_CLIENT_SECRET" \
"$DATABRICKS_WORKSPACE_URL/oidc/v1/token" \
--data 'grant_type=client_credentials&scope=all-apis' \
| jq -r .access_token)"
export ZEROBUS_UC_TABLE_JSON="$(curl -sS --fail \
-H "Authorization: Bearer $DATABRICKS_TOKEN" \
"$DATABRICKS_WORKSPACE_URL/api/2.1/unity-catalog/tables/$ZEROBUS_TABLE_NAME")"How to get these values:
- ZEROBUS_SERVER_ENDPOINT — Zerobus ingestion endpoint (usually
https://<shard-id>.zerobus.<region>.cloud.databricks.com). - DATABRICKS_WORKSPACE_URL — your Databricks workspace URL (Unity Catalog endpoint).
- ZEROBUS_TABLE_NAME — full table name in the form
catalog.schema.table. - DATABRICKS_CLIENT_ID / DATABRICKS_CLIENT_SECRET — OAuth 2.0 credentials from your service principal.
The examples build as part of the SDK's top-level CMake build, gated on the
ZEROBUS_BUILD_EXAMPLES option (on by default for a top-level build). From
cpp/:
make build # configure + build the SDK, tests, and examplesOr drive CMake directly:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jThe binaries land in build/examples/:
./build/examples/json_single
./build/examples/json_batch
./build/examples/proto_single
./build/examples/proto_batch
./build/examples/arrow_ingest # only if Apache Arrow C++ is installedThe JSON and proto examples need no extra dependencies. The Arrow example
requires the Apache Arrow C++ library (find_package(Arrow)); if it is not
installed the arrow_ingest target is skipped and the other four still build.
All examples follow the same general flow.
// server_endpoint, workspace_url, and table_name are read from the environment
// (ZEROBUS_SERVER_ENDPOINT / DATABRICKS_WORKSPACE_URL / ZEROBUS_TABLE_NAME).
zerobus::Sdk sdk = zerobus::Sdk::builder()
.endpoint(server_endpoint)
.unity_catalog_url(workspace_url)
.application_name("my-app")
.build();JSON:
zerobus::TableProperties props;
props.table_name = table_name; // empty descriptor => JSON stream
zerobus::StreamOptions options;
options.record_type = zerobus::RecordType::Json;
zerobus::Stream stream =
sdk.create_stream(props, client_id, client_secret, options);Protocol Buffers (dynamic schema):
zerobus::ProtoSchema schema = zerobus::ProtoSchema::from_uc_json(uc_table_json);
zerobus::TableProperties props;
props.table_name = table_name;
props.descriptor_proto = schema.descriptor_bytes();
zerobus::StreamOptions options;
options.record_type = zerobus::RecordType::Proto;
zerobus::Stream stream =
sdk.create_stream(props, client_id, client_secret, options);Arrow Flight (Beta):
zerobus::ArrowStream stream =
sdk.create_arrow_stream(table_name, schema_ipc_bytes, client_id,
client_secret);The cardinal performance rule.
ingest_*returns as soon as the record is queued — sending and acknowledgement happen on background tasks. Never callwait_for_offset()/flush()after every record. Waiting per record forces one server round-trip per record and collapses throughput. Ingest in a loop, thenflush()once at the end (or periodically for a continuous stream).
for (const auto& record : records) {
stream.ingest_json_record(record); // queue only — do NOT wait here
}
stream.flush(); // wait once for all pending ackswait_for_offset() behaves the same way: acks are monotonic, so waiting on the
last offset returned by a run of ingests confirms all prior ones too.
stream.close(); // flush + close at a controlled point; surfaces final errorsPrefer calling close() explicitly rather than relying on the destructor: it
flushes pending records and surfaces any error by throwing, whereas the
destructor swallows it.
| Aspect | Single-Record | Batch |
|---|---|---|
| Method | ingest_json_record() / ingest_proto_record() |
ingest_json_records() / ingest_proto_records() |
| Use case | Records arrive one at a time | Multiple records ready at once |
| Semantics | Each record independent | All-or-nothing (atomic) |
| Acknowledgment | Per record | Per batch (one offset for the batch) |
| Throughput | Lower | Higher (amortizes the FFI crossing) |
Single-record:
for (const auto& record : records) {
stream.ingest_json_record(record);
}
stream.flush();Batch:
std::int64_t batch_offset = stream.ingest_json_records(records);
if (batch_offset >= 0) {
stream.wait_for_offset(batch_offset); // one wait confirms the whole batch
}An empty batch is a no-op and returns -1.
| Feature | JSON | Protocol Buffers (dynamic) |
|---|---|---|
| Setup | Simple — no schema | Fetches the descriptor from Unity Catalog at runtime |
| Build deps | none | none (ProtoSchema, no protoc) |
| Type Safety | Runtime validation | Runtime validation against the fetched schema |
| Performance | Text-based | Efficient binary encoding |
| Best For | Prototyping, flexible schemas | Production, high-throughput |
Recommendation: Start with JSON for quick prototyping, then move to Protocol Buffers for production where binary encoding and performance matter. Both paths need no protobuf toolchain in your build.
Possible causes:
- Invalid credentials (client ID or secret).
- Service principal lacks permissions on the table.
- Incorrect workspace URL or endpoint.
- Table doesn't exist.
Solution: Verify your credentials and table permissions, and confirm the endpoint and workspace URL.
The example exits with code 2 before touching the SDK if a required environment
variable is missing. Export ZEROBUS_SERVER_ENDPOINT, DATABRICKS_WORKSPACE_URL,
ZEROBUS_TABLE_NAME, DATABRICKS_CLIENT_ID, and DATABRICKS_CLIENT_SECRET (and
ZEROBUS_UC_TABLE_JSON for the proto examples).
Possible causes: OAuth credentials expired or invalid, or an incorrect Unity Catalog endpoint. Solution: regenerate your service principal credentials and verify the workspace URL.
Possible causes: the JSON record doesn't match the table schema, invalid JSON
syntax, or a type mismatch (e.g. a string where a number is expected). For proto,
ProtoSchema::encode_json throws if a record can't be encoded. Solution:
verify your record structure matches the Databricks table schema exactly.
Apache Arrow C++ is not installed. Install it (e.g. libarrow-dev on Debian/
Ubuntu) and re-run CMake; the target appears automatically.