|
| 1 | +<!-- |
| 2 | +Licensed to the Apache Software Foundation (ASF) under one |
| 3 | +or more contributor license agreements. See the NOTICE file |
| 4 | +distributed with this work for additional information |
| 5 | +regarding copyright ownership. The ASF licenses this file |
| 6 | +to you under the Apache License, Version 2.0 (the |
| 7 | +"License"); you may not use this file except in compliance |
| 8 | +with the License. You may obtain a copy of the License at |
| 9 | +
|
| 10 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | +
|
| 12 | +Unless required by applicable law or agreed to in writing, |
| 13 | +software distributed under the License is distributed on an |
| 14 | +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | +KIND, either express or implied. See the License for the |
| 16 | +specific language governing permissions and limitations |
| 17 | +under the License. |
| 18 | +--> |
| 19 | + |
| 20 | +# C Integration |
| 21 | + |
| 22 | +The C integration exposes Apache Paimon Rust through a C ABI. It provides |
| 23 | +catalog and table access, scan planning, predicate push-down, streaming reads, |
| 24 | +writes and commits, and vector search. Record batches cross the ABI through the |
| 25 | +[Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html). |
| 26 | + |
| 27 | +The C binding is currently built from source. The repository does not check in |
| 28 | +a generated header or publish pre-built C packages. |
| 29 | + |
| 30 | +## Prerequisites |
| 31 | + |
| 32 | +- A Rust toolchain supported by this repository |
| 33 | +- A C11-compatible compiler |
| 34 | +- [`cbindgen`](https://github.com/mozilla/cbindgen) for generating the C header |
| 35 | +- An Arrow implementation if the application reads or writes record batches |
| 36 | + |
| 37 | +Install `cbindgen` when it is not already available: |
| 38 | + |
| 39 | +```bash |
| 40 | +cargo install cbindgen --locked |
| 41 | +``` |
| 42 | + |
| 43 | +## Building the Library and Header |
| 44 | + |
| 45 | +Run the following commands from the repository root: |
| 46 | + |
| 47 | +```bash |
| 48 | +cargo build --release -p paimon-c |
| 49 | +cbindgen bindings/c --lang c --output target/release/paimon.h |
| 50 | +``` |
| 51 | + |
| 52 | +The build produces a dynamic library and a static library under |
| 53 | +`target/release/`. Dynamic library names are platform-specific: |
| 54 | + |
| 55 | +| Platform | Dynamic library | |
| 56 | +|----------|-----------------| |
| 57 | +| Linux | `libpaimon_c.so` | |
| 58 | +| macOS | `libpaimon_c.dylib` | |
| 59 | +| Windows | `paimon_c.dll` | |
| 60 | + |
| 61 | +Link the generated header and library into an application: |
| 62 | + |
| 63 | +```bash |
| 64 | +cc -std=c11 example.c \ |
| 65 | + -Itarget/release \ |
| 66 | + -Ltarget/release \ |
| 67 | + -lpaimon_c \ |
| 68 | + -o example |
| 69 | +``` |
| 70 | + |
| 71 | +Make the dynamic library visible when running the executable. For example: |
| 72 | + |
| 73 | +```bash |
| 74 | +# Linux |
| 75 | +LD_LIBRARY_PATH=target/release ./example /path/to/warehouse |
| 76 | + |
| 77 | +# macOS |
| 78 | +DYLD_LIBRARY_PATH=target/release ./example /path/to/warehouse |
| 79 | +``` |
| 80 | + |
| 81 | +## Opening and Scanning a Table |
| 82 | + |
| 83 | +The following program opens `default.my_table` from a filesystem catalog and |
| 84 | +plans its data splits. Result structs contain either the requested handle or a |
| 85 | +non-null error. |
| 86 | + |
| 87 | +```c |
| 88 | +#include <stdint.h> |
| 89 | +#include <stdio.h> |
| 90 | +#include <stdlib.h> |
| 91 | + |
| 92 | +#include "paimon.h" |
| 93 | + |
| 94 | +#define CHECK_RESULT(result) \ |
| 95 | + do { \ |
| 96 | + if ((result).error != NULL) { \ |
| 97 | + fprintf(stderr, "Paimon error %d: %.*s\n", \ |
| 98 | + (result).error->code, \ |
| 99 | + (int)(result).error->message.len, \ |
| 100 | + (const char *)(result).error->message.data); \ |
| 101 | + paimon_error_free((result).error); \ |
| 102 | + goto cleanup; \ |
| 103 | + } \ |
| 104 | + } while (0) |
| 105 | + |
| 106 | +int main(int argc, char **argv) { |
| 107 | + int status = EXIT_FAILURE; |
| 108 | + paimon_catalog *catalog = NULL; |
| 109 | + paimon_identifier *identifier = NULL; |
| 110 | + paimon_table *table = NULL; |
| 111 | + paimon_read_builder *read_builder = NULL; |
| 112 | + paimon_table_scan *scan = NULL; |
| 113 | + paimon_plan *plan = NULL; |
| 114 | + |
| 115 | + if (argc != 2) { |
| 116 | + fprintf(stderr, "usage: %s WAREHOUSE\n", argv[0]); |
| 117 | + return EXIT_FAILURE; |
| 118 | + } |
| 119 | + |
| 120 | + paimon_option options[] = { |
| 121 | + {.key = "warehouse", .value = argv[1]}, |
| 122 | + }; |
| 123 | + paimon_result_catalog_new catalog_result = |
| 124 | + paimon_catalog_create(options, 1); |
| 125 | + CHECK_RESULT(catalog_result); |
| 126 | + catalog = catalog_result.catalog; |
| 127 | + |
| 128 | + paimon_result_identifier_new identifier_result = |
| 129 | + paimon_identifier_new("default", "my_table"); |
| 130 | + CHECK_RESULT(identifier_result); |
| 131 | + identifier = identifier_result.identifier; |
| 132 | + |
| 133 | + paimon_result_get_table table_result = |
| 134 | + paimon_catalog_get_table(catalog, identifier); |
| 135 | + CHECK_RESULT(table_result); |
| 136 | + table = table_result.table; |
| 137 | + |
| 138 | + paimon_result_read_builder builder_result = |
| 139 | + paimon_table_new_read_builder(table); |
| 140 | + CHECK_RESULT(builder_result); |
| 141 | + read_builder = builder_result.read_builder; |
| 142 | + |
| 143 | + paimon_result_table_scan scan_result = |
| 144 | + paimon_read_builder_new_scan(read_builder); |
| 145 | + CHECK_RESULT(scan_result); |
| 146 | + scan = scan_result.scan; |
| 147 | + |
| 148 | + paimon_result_plan plan_result = paimon_table_scan_plan(scan); |
| 149 | + CHECK_RESULT(plan_result); |
| 150 | + plan = plan_result.plan; |
| 151 | + |
| 152 | + printf("planned splits: %zu\n", paimon_plan_num_splits(plan)); |
| 153 | + status = EXIT_SUCCESS; |
| 154 | + |
| 155 | +cleanup: |
| 156 | + paimon_plan_free(plan); |
| 157 | + paimon_table_scan_free(scan); |
| 158 | + paimon_read_builder_free(read_builder); |
| 159 | + paimon_table_free(table); |
| 160 | + paimon_identifier_free(identifier); |
| 161 | + paimon_catalog_free(catalog); |
| 162 | + return status; |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +Catalog options are the same options accepted by the Rust catalog factory. For |
| 167 | +example, a REST catalog can be created with: |
| 168 | + |
| 169 | +```c |
| 170 | +paimon_option options[] = { |
| 171 | + {.key = "metastore", .value = "rest"}, |
| 172 | + {.key = "uri", .value = "http://localhost:8080"}, |
| 173 | + {.key = "warehouse", .value = "my_warehouse"}, |
| 174 | +}; |
| 175 | + |
| 176 | +paimon_result_catalog_new result = paimon_catalog_create(options, 3); |
| 177 | +``` |
| 178 | + |
| 179 | +## Reading Arrow Record Batches |
| 180 | + |
| 181 | +Paimon uses a **scan-then-read** flow. A scan creates a plan, and a table read |
| 182 | +consumes a range of that plan's splits through a streaming Arrow reader: |
| 183 | + |
| 184 | +```c |
| 185 | +paimon_result_new_read read_result = |
| 186 | + paimon_read_builder_new_read(read_builder); |
| 187 | +CHECK_RESULT(read_result); |
| 188 | +paimon_table_read *read = read_result.read; |
| 189 | + |
| 190 | +size_t split_count = paimon_plan_num_splits(plan); |
| 191 | +paimon_result_record_batch_reader reader_result = |
| 192 | + paimon_table_read_to_arrow(read, plan, 0, split_count); |
| 193 | +CHECK_RESULT(reader_result); |
| 194 | +paimon_record_batch_reader *reader = reader_result.reader; |
| 195 | + |
| 196 | +for (;;) { |
| 197 | + paimon_result_next_batch next = paimon_record_batch_reader_next(reader); |
| 198 | + CHECK_RESULT(next); |
| 199 | + |
| 200 | + if (next.batch.array == NULL && next.batch.schema == NULL) { |
| 201 | + break; /* End of stream. */ |
| 202 | + } |
| 203 | + |
| 204 | + /* Import next.batch.array and next.batch.schema with an Arrow C Data |
| 205 | + Interface consumer before freeing their container structs. */ |
| 206 | + paimon_arrow_batch_free(next.batch); |
| 207 | +} |
| 208 | + |
| 209 | +paimon_record_batch_reader_free(reader); |
| 210 | +paimon_table_read_free(read); |
| 211 | +``` |
| 212 | +
|
| 213 | +`paimon_table_read_to_arrow` accepts an `offset` and `length`, so separate |
| 214 | +workers can process disjoint contiguous ranges of the same plan. The requested |
| 215 | +range is clamped to the number of available splits. |
| 216 | +
|
| 217 | +!!! note "Arrow ownership" |
| 218 | + After importing a returned batch with the Arrow C Data Interface, call |
| 219 | + `paimon_arrow_batch_free` to release the heap-allocated `ArrowArray` and |
| 220 | + `ArrowSchema` container structs. When writing, the ownership direction is |
| 221 | + reversed: `paimon_table_write_write_arrow_batch` consumes the exported |
| 222 | + Arrow structures, so the caller must not release them again. |
| 223 | +
|
| 224 | +## Projection and Predicates |
| 225 | +
|
| 226 | +Projection uses a null-terminated array of column names: |
| 227 | +
|
| 228 | +```c |
| 229 | +const char *columns[] = {"id", "name", NULL}; |
| 230 | +paimon_error *error = |
| 231 | + paimon_read_builder_with_projection(read_builder, columns); |
| 232 | +if (error != NULL) { |
| 233 | + /* Inspect error->code and error->message, then free the error. */ |
| 234 | + paimon_error_free(error); |
| 235 | +} |
| 236 | +``` |
| 237 | + |
| 238 | +Predicate literals are passed as a tagged `paimon_datum`. This example builds |
| 239 | +`id = 42` for an `INT` column and transfers the predicate to the read builder: |
| 240 | + |
| 241 | +```c |
| 242 | +paimon_datum value = {0}; |
| 243 | +value.tag = 3; /* INT */ |
| 244 | +value.int_val = 42; |
| 245 | + |
| 246 | +paimon_result_predicate predicate_result = |
| 247 | + paimon_predicate_equal(table, "id", value); |
| 248 | +CHECK_RESULT(predicate_result); |
| 249 | + |
| 250 | +paimon_error *error = paimon_read_builder_with_filter( |
| 251 | + read_builder, predicate_result.predicate); |
| 252 | +if (error != NULL) { |
| 253 | + paimon_error_free(error); |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +The supported datum tags are: |
| 258 | + |
| 259 | +| Tag | Paimon type | Value fields | |
| 260 | +|-----|-------------|--------------| |
| 261 | +| 0 | `BOOL` | `int_val` (`0` or non-zero) | |
| 262 | +| 1–4 | `TINYINT`, `SMALLINT`, `INT`, `BIGINT` | `int_val` | |
| 263 | +| 5–6 | `FLOAT`, `DOUBLE` | `double_val` | |
| 264 | +| 7 | `STRING` | `str_data`, `str_len` | |
| 265 | +| 8–9 | `DATE`, `TIME` | `int_val` | |
| 266 | +| 10–11 | `TIMESTAMP`, `TIMESTAMP WITH LOCAL TIME ZONE` | `int_val`, `int_val2` | |
| 267 | +| 12 | `DECIMAL` | `int_val`, `int_val2`, `uint_val`, `uint_val2` | |
| 268 | +| 13 | `BYTES` | `str_data`, `str_len` | |
| 269 | + |
| 270 | +Leaf constructors support comparisons, null checks, `IN`, string operations, |
| 271 | +and ranges. Combine predicates with `paimon_predicate_and`, |
| 272 | +`paimon_predicate_or`, and `paimon_predicate_not`. |
| 273 | + |
| 274 | +!!! warning "Predicate ownership" |
| 275 | + `paimon_read_builder_with_filter`, the compound predicate functions, and |
| 276 | + `paimon_vector_search_builder_with_filter` consume their predicate inputs. |
| 277 | + Do not reuse or free a predicate after passing it to one of these functions. |
| 278 | + A predicate that has not been consumed must be released with |
| 279 | + `paimon_predicate_free`. |
| 280 | + |
| 281 | +## Writing and Committing |
| 282 | + |
| 283 | +Writing uses a **write-then-commit** flow: |
| 284 | + |
| 285 | +1. Create one `paimon_write_builder` from the table. |
| 286 | +2. Create a `paimon_table_write` and `paimon_table_commit` from that same |
| 287 | + builder. |
| 288 | +3. Export each record batch through the Arrow C Data Interface and pass it to |
| 289 | + `paimon_table_write_write_arrow_batch`. |
| 290 | +4. Call `paimon_table_write_prepare_commit` to obtain commit messages. |
| 291 | +5. Pass the messages to `paimon_table_commit_commit`. |
| 292 | +6. Free the messages, writer, committer, and builder. |
| 293 | + |
| 294 | +The input Arrow schema must match the table schema exactly, including field |
| 295 | +count, order, names, and types. A non-nullable table field must not contain null |
| 296 | +values. |
| 297 | + |
| 298 | +!!! warning "Write builder consistency" |
| 299 | + The writer and committer must be created from the same write builder because |
| 300 | + they share a commit identity. Commit messages must be freed with |
| 301 | + `paimon_commit_messages_free` even after a successful commit. The caller |
| 302 | + retains message ownership and may retry a failed commit. |
| 303 | + |
| 304 | +## Error Handling and Resource Ownership |
| 305 | + |
| 306 | +Functions that can fail use one of two conventions: |
| 307 | + |
| 308 | +- Constructors and terminal operations return a result struct with an `error` |
| 309 | + field. On success, `error` is null. |
| 310 | +- Mutating builder functions return `paimon_error *` directly. A null pointer |
| 311 | + means success. |
| 312 | + |
| 313 | +Error messages are byte buffers and are not null-terminated. Read exactly |
| 314 | +`error->message.len` bytes, and release the entire error with |
| 315 | +`paimon_error_free`. Do not separately call `paimon_bytes_free` on an error |
| 316 | +message. |
| 317 | + |
| 318 | +Every opaque handle returned by the C API has a matching free function. Free |
| 319 | +resources in reverse construction order. Pointer-based free functions accept |
| 320 | +null, which makes cleanup paths straightforward. |
0 commit comments