C Integration¶
The C integration exposes Apache Paimon Rust through a C ABI. It provides catalog and table access, scan planning, predicate push-down, streaming reads, writes and commits, and vector search. Record batches cross the ABI through the Arrow C Data Interface.
The C binding is currently built from source. The repository does not check in a generated header or publish pre-built C packages.
Prerequisites¶
- A Rust toolchain supported by this repository
- A C11-compatible compiler
cbindgenfor generating the C header- An Arrow implementation if the application reads or writes record batches
Install cbindgen when it is not already available:
Building the Library and Header¶
Run the following commands from the repository root:
The build produces a dynamic library and a static library under
target/release/. Dynamic library names are platform-specific:
| Platform | Dynamic library |
|---|---|
| Linux | libpaimon_c.so |
| macOS | libpaimon_c.dylib |
| Windows | paimon_c.dll |
Link the generated header and library into an application:
Make the dynamic library visible when running the executable. For example:
# Linux
LD_LIBRARY_PATH=target/release ./example /path/to/warehouse
# macOS
DYLD_LIBRARY_PATH=target/release ./example /path/to/warehouse
Opening and Scanning a Table¶
The following program opens default.my_table from a filesystem catalog and
plans its data splits. Result structs contain either the requested handle or a
non-null error.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "paimon.h"
#define CHECK_RESULT(result) \
do { \
if ((result).error != NULL) { \
fprintf(stderr, "Paimon error %d: %.*s\n", \
(result).error->code, \
(int)(result).error->message.len, \
(const char *)(result).error->message.data); \
paimon_error_free((result).error); \
goto cleanup; \
} \
} while (0)
int main(int argc, char **argv) {
int status = EXIT_FAILURE;
paimon_catalog *catalog = NULL;
paimon_identifier *identifier = NULL;
paimon_table *table = NULL;
paimon_read_builder *read_builder = NULL;
paimon_table_scan *scan = NULL;
paimon_plan *plan = NULL;
if (argc != 2) {
fprintf(stderr, "usage: %s WAREHOUSE\n", argv[0]);
return EXIT_FAILURE;
}
paimon_option options[] = {
{.key = "warehouse", .value = argv[1]},
};
paimon_result_catalog_new catalog_result =
paimon_catalog_create(options, 1);
CHECK_RESULT(catalog_result);
catalog = catalog_result.catalog;
paimon_result_identifier_new identifier_result =
paimon_identifier_new("default", "my_table");
CHECK_RESULT(identifier_result);
identifier = identifier_result.identifier;
paimon_result_get_table table_result =
paimon_catalog_get_table(catalog, identifier);
CHECK_RESULT(table_result);
table = table_result.table;
paimon_result_read_builder builder_result =
paimon_table_new_read_builder(table);
CHECK_RESULT(builder_result);
read_builder = builder_result.read_builder;
paimon_result_table_scan scan_result =
paimon_read_builder_new_scan(read_builder);
CHECK_RESULT(scan_result);
scan = scan_result.scan;
paimon_result_plan plan_result = paimon_table_scan_plan(scan);
CHECK_RESULT(plan_result);
plan = plan_result.plan;
printf("planned splits: %zu\n", paimon_plan_num_splits(plan));
status = EXIT_SUCCESS;
cleanup:
paimon_plan_free(plan);
paimon_table_scan_free(scan);
paimon_read_builder_free(read_builder);
paimon_table_free(table);
paimon_identifier_free(identifier);
paimon_catalog_free(catalog);
return status;
}
Catalog options are the same options accepted by the Rust catalog factory. For example, a REST catalog can be created with:
paimon_option options[] = {
{.key = "metastore", .value = "rest"},
{.key = "uri", .value = "http://localhost:8080"},
{.key = "warehouse", .value = "my_warehouse"},
};
paimon_result_catalog_new result = paimon_catalog_create(options, 3);
Reading Arrow Record Batches¶
Paimon uses a scan-then-read flow. A scan creates a plan, and a table read consumes a range of that plan's splits through a streaming Arrow reader:
paimon_result_new_read read_result =
paimon_read_builder_new_read(read_builder);
CHECK_RESULT(read_result);
paimon_table_read *read = read_result.read;
size_t split_count = paimon_plan_num_splits(plan);
paimon_result_record_batch_reader reader_result =
paimon_table_read_to_arrow(read, plan, 0, split_count);
CHECK_RESULT(reader_result);
paimon_record_batch_reader *reader = reader_result.reader;
for (;;) {
paimon_result_next_batch next = paimon_record_batch_reader_next(reader);
CHECK_RESULT(next);
if (next.batch.array == NULL && next.batch.schema == NULL) {
break; /* End of stream. */
}
/* Import next.batch.array and next.batch.schema with an Arrow C Data
Interface consumer before freeing their container structs. */
paimon_arrow_batch_free(next.batch);
}
paimon_record_batch_reader_free(reader);
paimon_table_read_free(read);
paimon_table_read_to_arrow accepts an offset and length, so separate
workers can process disjoint contiguous ranges of the same plan. The requested
range is clamped to the number of available splits.
Arrow ownership
After importing a returned batch with the Arrow C Data Interface, call
paimon_arrow_batch_free to release the heap-allocated ArrowArray and
ArrowSchema container structs. When writing, the ownership direction is
reversed: paimon_table_write_write_arrow_batch consumes the exported
Arrow structures, so the caller must not release them again.
Projection and Predicates¶
Projection uses a null-terminated array of column names:
const char *columns[] = {"id", "name", NULL};
paimon_error *error =
paimon_read_builder_with_projection(read_builder, columns);
if (error != NULL) {
/* Inspect error->code and error->message, then free the error. */
paimon_error_free(error);
}
Predicate literals are passed as a tagged paimon_datum. This example builds
id = 42 for an INT column and transfers the predicate to the read builder:
paimon_datum value = {0};
value.tag = 3; /* INT */
value.int_val = 42;
paimon_result_predicate predicate_result =
paimon_predicate_equal(table, "id", value);
CHECK_RESULT(predicate_result);
paimon_error *error = paimon_read_builder_with_filter(
read_builder, predicate_result.predicate);
if (error != NULL) {
paimon_error_free(error);
}
The supported datum tags are:
| Tag | Paimon type | Value fields |
|---|---|---|
| 0 | BOOL |
int_val (0 or non-zero) |
| 1–4 | TINYINT, SMALLINT, INT, BIGINT |
int_val |
| 5–6 | FLOAT, DOUBLE |
double_val |
| 7 | STRING |
str_data, str_len |
| 8–9 | DATE, TIME |
int_val |
| 10–11 | TIMESTAMP, TIMESTAMP WITH LOCAL TIME ZONE |
int_val, int_val2 |
| 12 | DECIMAL |
int_val, int_val2, uint_val, uint_val2 |
| 13 | BYTES |
str_data, str_len |
Leaf constructors support comparisons, null checks, IN, string operations,
and ranges. Combine predicates with paimon_predicate_and,
paimon_predicate_or, and paimon_predicate_not.
Predicate ownership
paimon_read_builder_with_filter, the compound predicate functions, and
paimon_vector_search_builder_with_filter consume their predicate inputs.
Do not reuse or free a predicate after passing it to one of these functions.
A predicate that has not been consumed must be released with
paimon_predicate_free.
Writing and Committing¶
Writing uses a write-then-commit flow:
- Create one
paimon_write_builderfrom the table. - Create a
paimon_table_writeandpaimon_table_commitfrom that same builder. - Export each record batch through the Arrow C Data Interface and pass it to
paimon_table_write_write_arrow_batch. - Call
paimon_table_write_prepare_committo obtain commit messages. - Pass the messages to
paimon_table_commit_commit. - Free the messages, writer, committer, and builder.
The input Arrow schema must match the table schema exactly, including field count, order, names, and types. A non-nullable table field must not contain null values.
Write builder consistency
The writer and committer must be created from the same write builder because
they share a commit identity. Commit messages must be freed with
paimon_commit_messages_free even after a successful commit. The caller
retains message ownership and may retry a failed commit.
Error Handling and Resource Ownership¶
Functions that can fail use one of two conventions:
- Constructors and terminal operations return a result struct with an
errorfield. On success,erroris null. - Mutating builder functions return
paimon_error *directly. A null pointer means success.
Error messages are byte buffers and are not null-terminated. Read exactly
error->message.len bytes, and release the entire error with
paimon_error_free. Do not separately call paimon_bytes_free on an error
message.
Every opaque handle returned by the C API has a matching free function. Free resources in reverse construction order. Pointer-based free functions accept null, which makes cleanup paths straightforward.