Skip to main content

C++ API

Paimon C++ provides native table access for C++ applications and engines. It exchanges columnar data through the Arrow C Data Interface.

This walkthrough follows the same workflow as the Java API: create a catalog and table, prepare and commit a batch, then plan splits and read them. The snippets are function-body fragments returning paimon::Status (place includes at file scope); PAIMON_RETURN_NOT_OK and PAIMON_ASSIGN_OR_RAISE propagate failures to its caller. The PrepareData helper returns arrow::Result and uses Arrow's error macros instead.

For complete headers and a runnable application, start with the C++ examples. The C++ project has its own release cycle; use its build instructions and API reference for the version you select.

Environment Settings​

Follow the C++ build guide to install prerequisites and select optional filesystem, file-format, and catalog components. A basic source build is:

git clone https://github.com/apache/paimon-cpp.git
cd paimon-cpp
mkdir build-release
cd build-release
cmake ..
make -j8 # if you have 8 CPU cores, otherwise adjust
make install

Create Catalog​

Create a filesystem catalog for a warehouse. Reuse these options and identifiers in the following fragments; choose a fresh table name when running the walkthrough again.

#include "paimon/catalog/catalog.h"

const std::string root_path = "/tmp/paimon-cpp-warehouse";
const std::string db_name = "my_db";
const std::string table_name = "my_table";
std::map<std::string, std::string> options;
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::Catalog> catalog,
paimon::Catalog::Create(root_path, options));

C++ also supports a REST catalog when built with PAIMON_ENABLE_REST=ON. See the C++ catalog guide for options and supported operations. For REST-managed tables, use the table location returned by the catalog; the path construction below is specific to the filesystem catalog.

Create Database​

Create the database before the table:

PAIMON_RETURN_NOT_OK(catalog->CreateDatabase(db_name, options, /*ignore_if_exists=*/false));

Create Table​

Define fields using an arrow::Schema, then export it through the Arrow C Data Interface. This example creates an unpartitioned append table without primary keys.

arrow::FieldVector fields = {
arrow::field("f0", arrow::utf8()),
arrow::field("f1", arrow::int32()),
arrow::field("f2", arrow::int32()),
arrow::field("f3", arrow::float64()),
};
std::shared_ptr<arrow::Schema> schema = arrow::schema(fields);
::ArrowSchema arrow_schema;
arrow::Status arrow_status = arrow::ExportSchema(*schema, &arrow_schema);
if (!arrow_status.ok()) {
return paimon::Status::Invalid(arrow_status.message());
}
PAIMON_RETURN_NOT_OK(catalog->CreateTable(paimon::Identifier(db_name, table_name),
&arrow_schema,
/*partition_keys=*/{},
/*primary_keys=*/{}, options,
/*ignore_if_exists=*/false));

See Data Types for all supported arrow-to-paimon data types mapping.

Batch Write​

First construct an Arrow batch, then write it and prepare commit messages. The committer publishes the prepared changes. In a distributed application, collect messages from the participating writers before committing. See the memory format guide for ownership and Arrow conversion details.

Build a batch​

arrow::Result<std::shared_ptr<arrow::StructArray>> PrepareData(const arrow::FieldVector& fields) {
arrow::StringBuilder f0_builder;
arrow::Int32Builder f1_builder;
arrow::Int32Builder f2_builder;
arrow::DoubleBuilder f3_builder;

std::vector<std::tuple<std::string, int, int, double>> data = {
{"Alice", 1, 0, 11.0}, {"Bob", 1, 1, 12.1}, {"Cathy", 1, 2, 13.2}};

for (const auto& row : data) {
ARROW_RETURN_NOT_OK(f0_builder.Append(std::get<0>(row)));
ARROW_RETURN_NOT_OK(f1_builder.Append(std::get<1>(row)));
ARROW_RETURN_NOT_OK(f2_builder.Append(std::get<2>(row)));
ARROW_RETURN_NOT_OK(f3_builder.Append(std::get<3>(row)));
}

std::shared_ptr<arrow::Array> f0_array, f1_array, f2_array, f3_array;
ARROW_RETURN_NOT_OK(f0_builder.Finish(&f0_array));
ARROW_RETURN_NOT_OK(f1_builder.Finish(&f1_array));
ARROW_RETURN_NOT_OK(f2_builder.Finish(&f2_array));
ARROW_RETURN_NOT_OK(f3_builder.Finish(&f3_array));

std::vector<std::shared_ptr<arrow::Array>> children = {f0_array, f1_array, f2_array, f3_array};
auto struct_type = arrow::struct_(fields);
return std::make_shared<arrow::StructArray>(struct_type, f0_array->length(), children);
}

Write and commit​

std::string table_path = root_path + "/" + db_name + ".db/" + table_name;
std::string commit_user = "some_commit_user";
// write
paimon::WriteContextBuilder context_builder(table_path, commit_user);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::WriteContext> write_context,
context_builder.SetOptions(options).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::FileStoreWrite> writer,
paimon::FileStoreWrite::Create(std::move(write_context)));
// prepare data
auto struct_array = PrepareData(fields);
if (!struct_array.ok()) {
return paimon::Status::Invalid(struct_array.status().ToString());
}
::ArrowArray arrow_array;
arrow_status = arrow::ExportArray(*struct_array.ValueUnsafe(), &arrow_array);
if (!arrow_status.ok()) {
return paimon::Status::Invalid(arrow_status.message());
}
paimon::RecordBatchBuilder batch_builder(&arrow_array);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::RecordBatch> record_batch,
batch_builder.Finish());
PAIMON_RETURN_NOT_OK(writer->Write(std::move(record_batch)));
PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<paimon::CommitMessage>> commit_message,
writer->PrepareCommit());

// commit
paimon::CommitContextBuilder commit_context_builder(table_path, commit_user);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::CommitContext> commit_context,
commit_context_builder.SetOptions(options).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::FileStoreCommit> committer,
paimon::FileStoreCommit::Create(std::move(commit_context)));
PAIMON_RETURN_NOT_OK(committer->Commit(commit_message));

Batch Read​

Configure the reader, plan the splits, then consume each batch. When distributing reads, plan once and assign splits to reader tasks.

Predicate pushdown​

Use ReadContextBuilder to configure the reader. EnablePredicateFilter(true) requests row-level filtering as well as any pruning supported by the reader:

// Example filter: 'f3' > 12.0 OR 'f1' == 1
PAIMON_ASSIGN_OR_RAISE(
auto predicate,
paimon::PredicateBuilder::Or({
paimon::PredicateBuilder::GreaterThan(
/*field_index=*/3, /*field_name=*/"f3",
paimon::FieldType::DOUBLE, paimon::Literal(12.0)),
paimon::PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"f1",
paimon::FieldType::INT, paimon::Literal(1))}));
paimon::ReadContextBuilder read_context_builder(table_path);
read_context_builder.SetPredicate(predicate).EnablePredicateFilter(true);

Set the projected fields on the same read context:

// Return f3, f1, and f2, in that order
read_context_builder.SetReadFieldNames({"f3", "f1", "f2"});

Generate Splits​

Create a scan plan to discover the splits to read:

// scan
paimon::ScanContextBuilder scan_context_builder(table_path);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::ScanContext> scan_context,
scan_context_builder.SetOptions(options).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::TableScan> scanner,
paimon::TableScan::Create(std::move(scan_context)));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<paimon::Plan> plan, scanner->CreatePlan());
auto splits = plan->Splits();

Pass the planned splits to a table reader to obtain Arrow batches.

Read Apache Arrow​

Import each returned batch into Arrow C++ objects. This example collects the batches in memory; for large results, process batches as they arrive instead.

PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::ReadContext> read_context,
read_context_builder.SetOptions(options).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::TableRead> table_read,
paimon::TableRead::Create(std::move(read_context)));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<paimon::BatchReader> batch_reader,
table_read->CreateReader(splits));
arrow::ArrayVector result_array_vector;
while (true) {
PAIMON_ASSIGN_OR_RAISE(paimon::BatchReader::ReadBatch batch, batch_reader->NextBatch());
if (paimon::BatchReader::IsEofBatch(batch)) {
break;
}
auto& [c_array, c_schema] = batch;
auto arrow_result = arrow::ImportArray(c_array.get(), c_schema.get());
if (!arrow_result.ok()) {
return paimon::Status::Invalid(arrow_result.status().ToString());
}
auto result_array = arrow_result.ValueUnsafe();
result_array_vector.push_back(result_array);
}
auto chunk_result = arrow::ChunkedArray::Make(result_array_vector);
if (!chunk_result.ok()) {
return paimon::Status::Invalid(chunk_result.status().ToString());
}

Documentation​

For more information, see C++ Paimon Documentation.