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.
cbindgen for generating the C headerInstall cbindgen when it is not already available:
cargo install cbindgen --locked
Run the following commands from the repository root:
cargo build --release -p paimon-c cbindgen bindings/c --lang c --output target/release/paimon.h
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:
cc -std=c11 example.c \ -Itarget/release \ -Ltarget/release \ -lpaimon_c \ -o example
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
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);
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.
!!! note “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 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.
!!! warning “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 uses a write-then-commit flow:
paimon_write_builder from the table.paimon_table_write and paimon_table_commit from that same builder.paimon_table_write_write_arrow_batch.paimon_table_write_prepare_commit to obtain commit messages.paimon_table_commit_commit.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.
!!! warning “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.
Functions that can fail use one of two conventions:
error field. On success, error is null.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.