Skip to content

Commit 9fb4a2b

Browse files
authored
feat: add ArrowRowBuilder for materializing Arrow batches (#780)
## Summary Adds `ArrowRowBuilder` (`arrow_row_builder_internal.h` / `arrow_row_builder.cc`), a schema-driven RAII helper that materializes in-memory rows into an Arrow `ArrowArray` (a struct batch) for an arbitrary Iceberg schema. It wraps the nanoarrow boilerplate and exposes per-column access plus typed append free functions, so metadata tables (snapshots, history, manifests, …) can emit rows without re-implementing it. This is the first of a series splitting metadata-table support into focused PRs; the `InMemoryBatchReader` and the `SnapshotsTable::Scan` integration are intended to follow in separate PRs that build on this. ## What's included - **`ArrowRowBuilder`** — a single RAII class (move-only) with `Make(const Schema&)` and `Make(const ArrowSchema*)` overloads. Handles the full nanoarrow lifecycle: `InitFromSchema` → `StartAppending` → … append values … → `FinishBuilding` → `Release`. The `ArrowArray` is guarded immediately after `InitFromSchema` so a failure in `StartAppending` releases it automatically. - **`ArrowArrayGuard::Release()`** — added to the existing guard so other call sites (`position_delete_writer`, `manifest_adapter`) can reuse the RAII-release pattern instead of manually managing nanoarrow resources. - **Free functions** in the `iceberg` namespace: `AppendNull`, `AppendBoolean`, `AppendInt` (covers int32/int64/timestamp via nanoarrow's int64), `AppendString`, `AppendStringMap`. - The implementation lives at the **core** `iceberg` library level — it only needs nanoarrow + `ToArrowSchema` (no Apache Arrow), matching peers like `manifest_adapter` and `arrow_c_data_util`. - Unit tests in `arrow_row_builder_test.cc` covering typed appends (int32/string/int64/boolean/map), null handling for optional columns, multi-entry/empty string maps, zero-row batches, and column-index bounds. Compiled into the `iceberg-data-test` test target. ## Testing - CMake (Ninja): `cmake --build build --target iceberg-data-test` then ran the test binary — 5/5 `ArrowRowBuilderTest` tests pass. `ctest` green. - The test verifies output by importing the produced C-data into Apache Arrow (`arrow::ImportRecordBatch`), so its target is under `USE_BUNDLE`. ## Notes - The test is registered under CMake's bundle build only. The meson build (which has no Apache Arrow/bundle layer) is left unchanged; the core-only test target continues to build there. - Developed with AI-assisted tooling, reviewed by the author.
1 parent 7c4f15c commit 9fb4a2b

7 files changed

Lines changed: 456 additions & 2 deletions

File tree

src/iceberg/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818
set(ICEBERG_INCLUDES "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/src>"
1919
"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/src>")
2020
set(ICEBERG_SOURCES
21-
arrow_c_data_util.cc
2221
arrow_c_data_guard_internal.cc
22+
arrow_c_data_util.cc
23+
arrow_row_builder.cc
2324
catalog/memory/in_memory_catalog.cc
2425
catalog/session_catalog.cc
2526
catalog/session_context.cc

src/iceberg/arrow_c_data_guard_internal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ class ICEBERG_EXPORT ArrowArrayGuard {
3131
explicit ArrowArrayGuard(ArrowArray* array) : array_(array) {}
3232
~ArrowArrayGuard();
3333

34+
/// \brief Release the guard without calling ArrowArrayRelease.
35+
///
36+
/// Call this when ownership of the underlying ArrowArray has been
37+
/// transferred elsewhere and the guard should not release it.
38+
void Release() { array_ = nullptr; }
39+
3440
private:
3541
ArrowArray* array_;
3642
};

src/iceberg/arrow_row_builder.cc

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
#include <utility>
21+
22+
#include <nanoarrow/nanoarrow.h>
23+
24+
#include "iceberg/arrow/nanoarrow_status_internal.h"
25+
#include "iceberg/arrow_c_data_guard_internal.h"
26+
#include "iceberg/arrow_row_builder_internal.h"
27+
#include "iceberg/schema.h"
28+
#include "iceberg/schema_internal.h"
29+
30+
namespace iceberg {
31+
32+
Result<ArrowRowBuilder> ArrowRowBuilder::Make(const Schema& schema) {
33+
ArrowSchema arrow_schema;
34+
ICEBERG_RETURN_UNEXPECTED(ToArrowSchema(schema, &arrow_schema));
35+
internal::ArrowSchemaGuard schema_guard(&arrow_schema);
36+
return Make(&arrow_schema);
37+
}
38+
39+
Result<ArrowRowBuilder> ArrowRowBuilder::Make(const ArrowSchema* schema) {
40+
ArrowRowBuilder builder;
41+
ArrowError error;
42+
ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
43+
ArrowArrayInitFromSchema(&builder.array_, schema, &error), error);
44+
// Guard the array in case StartAppending fails.
45+
internal::ArrowArrayGuard guard(&builder.array_);
46+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayStartAppending(&builder.array_));
47+
// Ownership stays with the builder — disarm the guard.
48+
guard.Release();
49+
return builder;
50+
}
51+
52+
ArrowRowBuilder::ArrowRowBuilder(ArrowRowBuilder&& other) noexcept
53+
: array_(other.array_) {
54+
other.array_.release = nullptr;
55+
}
56+
57+
ArrowRowBuilder& ArrowRowBuilder::operator=(ArrowRowBuilder&& other) noexcept {
58+
if (this != &other) {
59+
if (array_.release != nullptr) {
60+
ArrowArrayRelease(&array_);
61+
}
62+
array_ = other.array_;
63+
other.array_.release = nullptr;
64+
}
65+
return *this;
66+
}
67+
68+
ArrowRowBuilder::~ArrowRowBuilder() {
69+
if (array_.release != nullptr) {
70+
ArrowArrayRelease(&array_);
71+
}
72+
}
73+
74+
int64_t ArrowRowBuilder::num_columns() const { return array_.n_children; }
75+
76+
ArrowArray* ArrowRowBuilder::column(int64_t index) {
77+
if (index < 0 || index >= array_.n_children) {
78+
return nullptr;
79+
}
80+
return array_.children[index];
81+
}
82+
83+
Status ArrowRowBuilder::FinishRow() {
84+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(&array_));
85+
return {};
86+
}
87+
88+
Result<ArrowArray> ArrowRowBuilder::Finish() && {
89+
ArrowError error;
90+
ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR(
91+
ArrowArrayFinishBuildingDefault(&array_, &error), error);
92+
ArrowArray result = array_;
93+
array_.release = nullptr;
94+
return result;
95+
}
96+
97+
Status AppendNull(ArrowArray* array) {
98+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendNull(array, 1));
99+
return {};
100+
}
101+
102+
Status AppendBoolean(ArrowArray* array, bool value) {
103+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendInt(array, value ? 1 : 0));
104+
return {};
105+
}
106+
107+
Status AppendInt(ArrowArray* array, int64_t value) {
108+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendInt(array, value));
109+
return {};
110+
}
111+
112+
Status AppendString(ArrowArray* array, std::string_view value) {
113+
ArrowStringView view(value.data(), static_cast<int64_t>(value.size()));
114+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayAppendString(array, view));
115+
return {};
116+
}
117+
118+
Status AppendStringMap(ArrowArray* array,
119+
const std::unordered_map<std::string, std::string>& entries) {
120+
// A nanoarrow map array is a list of struct<key, value>. children[0] is the
121+
// entries struct, whose children[0]/children[1] are the key/value builders.
122+
ArrowArray* struct_array = array->children[0];
123+
ArrowArray* key_array = struct_array->children[0];
124+
ArrowArray* value_array = struct_array->children[1];
125+
126+
for (const auto& [key, value] : entries) {
127+
ICEBERG_RETURN_UNEXPECTED(AppendString(key_array, key));
128+
ICEBERG_RETURN_UNEXPECTED(AppendString(value_array, value));
129+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(struct_array));
130+
}
131+
132+
// Finish the (possibly empty) map element on the outer list.
133+
ICEBERG_NANOARROW_RETURN_UNEXPECTED(ArrowArrayFinishElement(array));
134+
return {};
135+
}
136+
137+
} // namespace iceberg
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
#pragma once
21+
22+
/// \file iceberg/arrow_row_builder_internal.h
23+
/// Internal Arrow row-building utilities shared by metadata tables.
24+
///
25+
/// Metadata tables (snapshots, history, manifests, ...) materialize in-memory
26+
/// structures into Arrow batches that conform to the table's Iceberg schema.
27+
/// `ArrowRowBuilder` wraps a nanoarrow `ArrowArray` initialized from such a
28+
/// schema and exposes per-column access plus typed append helpers so each
29+
/// metadata table can emit rows without re-implementing the nanoarrow
30+
/// boilerplate.
31+
32+
#include <cstdint>
33+
#include <string_view>
34+
#include <unordered_map>
35+
36+
#include "iceberg/arrow_c_data.h"
37+
#include "iceberg/iceberg_export.h"
38+
#include "iceberg/result.h"
39+
#include "iceberg/type_fwd.h"
40+
41+
namespace iceberg {
42+
43+
/// \brief Movable RAII builder that materializes rows into an Arrow struct array.
44+
///
45+
/// Handles the nanoarrow lifecycle: InitFromSchema → StartAppending →
46+
/// ... append values ... → FinishBuilding → Release.
47+
///
48+
/// Two constructors:
49+
/// - `Make(schema)` accepts an Iceberg Schema (typical for metadata tables).
50+
/// - `Make(arrow_schema)` accepts a raw ArrowSchema (for lower-level callers
51+
/// like position_delete_writer or manifest_adapter).
52+
///
53+
/// Typical usage:
54+
/// \code
55+
/// ICEBERG_ASSIGN_OR_RAISE(auto builder, ArrowRowBuilder::Make(schema));
56+
/// for (const auto& row : rows) {
57+
/// ICEBERG_RETURN_UNEXPECTED(AppendInt(builder.column(0), row.id));
58+
/// ICEBERG_RETURN_UNEXPECTED(AppendString(builder.column(1), row.name));
59+
/// ICEBERG_RETURN_UNEXPECTED(builder.FinishRow());
60+
/// }
61+
/// ICEBERG_ASSIGN_OR_RAISE(auto array, std::move(builder).Finish());
62+
/// \endcode
63+
class ICEBERG_EXPORT ArrowRowBuilder {
64+
public:
65+
/// \brief Create a row builder from an Iceberg schema.
66+
static Result<ArrowRowBuilder> Make(const Schema& schema);
67+
68+
/// \brief Create a row builder from an ArrowSchema.
69+
///
70+
/// The schema must outlive this call (the caller guards it). On failure the
71+
/// partially-initialized array is released automatically.
72+
static Result<ArrowRowBuilder> Make(const ArrowSchema* schema);
73+
74+
ArrowRowBuilder(ArrowRowBuilder&& other) noexcept;
75+
ArrowRowBuilder& operator=(ArrowRowBuilder&& other) noexcept;
76+
77+
ArrowRowBuilder(const ArrowRowBuilder&) = delete;
78+
ArrowRowBuilder& operator=(const ArrowRowBuilder&) = delete;
79+
80+
~ArrowRowBuilder();
81+
82+
/// \brief The number of top-level columns in the batch.
83+
int64_t num_columns() const;
84+
85+
/// \brief Access the nanoarrow child builder for a top-level column.
86+
///
87+
/// \param index Zero-based column index. Returns nullptr if out of range.
88+
ArrowArray* column(int64_t index);
89+
90+
/// \brief Finish the current row, advancing the struct length by one.
91+
///
92+
/// Call after appending exactly one value (or null) to every column.
93+
Status FinishRow();
94+
95+
/// \brief Finish building and transfer ownership of the resulting array.
96+
///
97+
/// The builder must not be used after this call.
98+
Result<ArrowArray> Finish() &&;
99+
100+
private:
101+
ArrowRowBuilder() = default;
102+
ArrowArray array_{};
103+
};
104+
105+
/// \brief Append a null to a nanoarrow array builder.
106+
ICEBERG_EXPORT Status AppendNull(ArrowArray* array);
107+
108+
/// \brief Append a boolean value to a nanoarrow array builder.
109+
ICEBERG_EXPORT Status AppendBoolean(ArrowArray* array, bool value);
110+
111+
/// \brief Append an integer value to a nanoarrow array builder.
112+
///
113+
/// Works for int32/int64/timestamp columns, which nanoarrow stores as int64.
114+
ICEBERG_EXPORT Status AppendInt(ArrowArray* array, int64_t value);
115+
116+
/// \brief Append a string value to a nanoarrow array builder.
117+
ICEBERG_EXPORT Status AppendString(ArrowArray* array, std::string_view value);
118+
119+
/// \brief Append a map<string, string> value to a nanoarrow map array builder.
120+
///
121+
/// Appends one (possibly empty) map element. The iteration order of the
122+
/// resulting entries is unspecified.
123+
ICEBERG_EXPORT Status AppendStringMap(
124+
ArrowArray* array, const std::unordered_map<std::string, std::string>& entries);
125+
126+
} // namespace iceberg

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ iceberg_include_dir = include_directories('..')
4545
iceberg_sources = files(
4646
'arrow_c_data_guard_internal.cc',
4747
'arrow_c_data_util.cc',
48+
'arrow_row_builder.cc',
4849
'catalog/memory/in_memory_catalog.cc',
4950
'catalog/session_catalog.cc',
5051
'catalog/session_context.cc',

src/iceberg/test/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ add_iceberg_test(schema_test
8989
add_iceberg_test(table_test
9090
SOURCES
9191
location_provider_test.cc
92-
metadata_table_test.cc
9392
metrics_config_test.cc
9493
metrics_reporter_test.cc
9594
metrics_test.cc
@@ -189,6 +188,8 @@ if(ICEBERG_BUILD_BUNDLE)
189188

190189
add_iceberg_test(catalog_test USE_BUNDLE SOURCES in_memory_catalog_test.cc)
191190

191+
add_iceberg_test(metadata_table_test USE_BUNDLE SOURCES metadata_table_test.cc)
192+
192193
add_iceberg_test(eval_expr_test
193194
USE_BUNDLE
194195
SOURCES
@@ -251,6 +252,7 @@ if(ICEBERG_BUILD_BUNDLE)
251252
USE_BUNDLE
252253
SOURCES
253254
arrow_c_data_util_test.cc
255+
arrow_row_builder_test.cc
254256
data_writer_test.cc
255257
delete_filter_test.cc
256258
delete_loader_test.cc

0 commit comments

Comments
 (0)