Skip to content

Commit 9bcd402

Browse files
authored
feat: Add Apache Arrow schema mapper generation to code generator (#966)
1 parent 7999a99 commit 9bcd402

11 files changed

Lines changed: 272 additions & 2 deletions

File tree

.github/workflows/sanitizers.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ jobs:
4444
cmake -B build -S . \
4545
--preset ci-build \
4646
-DCMAKE_BUILD_TYPE=Debug \
47+
-DENABLE_ARROW=ON \
4748
-DUSE_SANITIZER=${{ matrix.sanitizer }} \
4849
-DCMAKE_CXX_STANDARD=$([[ ${{ matrix.compiler }} == gcc15 ]] && echo "23" || echo "20")
4950
echo "::endgroup::"

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ jobs:
3636
# export JULIA_DEPOT_PATH="$(mktemp -d -p /tmp -t julia_depot_XXXXX):"
3737
cmake --preset ci-build \
3838
-DENABLE_JULIA=ON \
39+
-DENABLE_ARROW=ON \
3940
-DENABLE_RNTUPLE=$([[ ${{ matrix.LCG }} == LCG_104/* ]] && echo "OFF" || echo "ON") \
4041
-DPODIO_RUN_STRACE_TEST=$([[ ${{ matrix.LCG }} == LCG_104/* ]] && echo "OFF" || echo "ON") \
4142
-DCMAKE_INSTALL_PREFIX=$(pwd)/install \

.github/workflows/ubuntu.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ jobs:
3232
echo "::group::Run CMake"
3333
cmake -S . --preset ci-build \
3434
-DENABLE_JULIA=ON \
35+
-DENABLE_ARROW=ON \
3536
-DCMAKE_INSTALL_PREFIX=../install \
3637
-DCMAKE_CXX_STANDARD=20 \
3738
-DUSE_EXTERNAL_CATCH2=OFF \

CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ ADD_CLANG_TIDY()
6767
#--- Declare options -----------------------------------------------------------
6868
option(CREATE_DOC "Whether or not to create doxygen doc target." OFF)
6969
option(ENABLE_SIO "Build SIO I/O support" OFF)
70+
option(ENABLE_ARROW "Build Arrow I/O support" OFF)
7071
option(PODIO_RELAX_PYVER "Do not require exact python version match with ROOT" OFF)
7172
option(ENABLE_RNTUPLE "Build with support for the new ROOT NTtuple format" OFF)
7273
option(ENABLE_DATASOURCE "Build podio's ROOT DataSource" OFF)
@@ -146,6 +147,15 @@ if(ENABLE_SIO)
146147
endif()
147148
endif()
148149

150+
# optionally build with Arrow -----------------------------------------------
151+
if(ENABLE_ARROW)
152+
find_package(Arrow REQUIRED)
153+
if(Arrow_FOUND)
154+
message(STATUS "Found Arrow library - will build Arrow I/O support")
155+
list(APPEND PODIO_IO_HANDLERS ARROW)
156+
endif()
157+
endif()
158+
149159
#--- enable unit testing capabilities ------------------------------------------
150160
include(CTest)
151161
set(USE_EXTERNAL_CATCH2 AUTO CACHE STRING "Link against an external Catch2 v3 static library, otherwise build it locally")

cmake/podioMacros.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ function(PODIO_ADD_DATAMODEL_CORE_LIB lib_name HEADERS SOURCES)
197197

198198
# Filter out anything I/O backend related to build the core library
199199
LIST(FILTER HEADERS EXCLUDE REGEX .*SIOBlock.h)
200+
LIST(FILTER HEADERS EXCLUDE REGEX .*ArrowMapper.h)
200201
LIST(FILTER SOURCES EXCLUDE REGEX .*SIOBlock.cc)
201202

202203
add_library(${lib_name} SHARED ${SOURCES} ${HEADERS})
@@ -250,6 +251,7 @@ function(PODIO_ADD_ROOT_IO_DICT dict_name CORE_LIB HEADERS SELECTION_XML)
250251
# Filter out anything I/O backend related from the generated headers as ROOT only needs
251252
# the core headers
252253
LIST(FILTER HEADERS EXCLUDE REGEX .*SIOBlock.h)
254+
LIST(FILTER HEADERS EXCLUDE REGEX .*ArrowMapper.h)
253255

254256
add_library(${dict_name} SHARED)
255257
target_link_libraries(${dict_name} PUBLIC

python/podio_class_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def parse_version(version_str):
105105
parser.add_argument("packagename", help="Name of the package.")
106106
parser.add_argument(
107107
"iohandlers",
108-
choices=["ROOT", "SIO"],
108+
choices=["ROOT", "SIO", "ARROW"],
109109
nargs="*",
110110
help="The IO backend specific code that should be generated",
111111
default="ROOT",

python/podio_gen/cpp_generator.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
)
1616
from podio_gen.podio_config_reader import PodioConfigReader
1717
from podio_gen.generator_base import ClassGeneratorBaseMixin, write_file_if_changed
18-
from podio_gen.generator_utils import DataType, DataModelJSONEncoder
18+
from podio_gen.generator_utils import DataType, DataModelJSONEncoder, DefinitionError
1919

2020
REPORT_TEXT = """
2121
PODIO Data Model
@@ -24,6 +24,34 @@
2424
Read instructions in the README.md to run your first example!
2525
"""
2626

27+
ARROW_PRIMITIVE_TYPES = {
28+
"bool": "arrow::boolean()",
29+
"char": "arrow::int8()",
30+
"short": "arrow::int16()",
31+
"int": "arrow::int32()",
32+
"long": "arrow::int64()",
33+
"long long": "arrow::int64()",
34+
"unsigned": "arrow::uint32()",
35+
"unsigned int": "arrow::uint32()",
36+
"unsigned long": "arrow::uint64()",
37+
"unsigned long long": "arrow::uint64()",
38+
"float": "arrow::float32()",
39+
"double": "arrow::float64()",
40+
"int16_t": "arrow::int16()",
41+
"int32_t": "arrow::int32()",
42+
"int64_t": "arrow::int64()",
43+
"uint16_t": "arrow::uint16()",
44+
"uint32_t": "arrow::uint32()",
45+
"uint64_t": "arrow::uint64()",
46+
"std::int16_t": "arrow::int16()",
47+
"std::int32_t": "arrow::int32()",
48+
"std::int64_t": "arrow::int64()",
49+
"std::uint16_t": "arrow::uint16()",
50+
"std::uint32_t": "arrow::uint32()",
51+
"std::uint64_t": "arrow::uint64()",
52+
"std::string": "arrow::utf8()",
53+
}
54+
2755

2856
class IncludeFrom(IntEnum):
2957
"""Enum to signify if an include is needed and from where it should come"""
@@ -85,6 +113,9 @@ def post_process(self, datamodel):
85113
if "ROOT" in self.io_handlers:
86114
self._create_selection_xml()
87115

116+
if "ARROW" in self.io_handlers:
117+
self._write_arrow_mapper_header(datamodel)
118+
88119
if the_links := datamodel["links"]:
89120
self._write_links_registration_file(the_links)
90121
self._write_all_collections_header()
@@ -122,6 +153,76 @@ def do_process_datatype(self, name, datatype):
122153

123154
return datatype
124155

156+
def _write_arrow_mapper_header(self, datamodel):
157+
"""A generated helper that exposes the datamodel as an Arrow schema"""
158+
datatypes = []
159+
for datatype in datamodel["datatypes"]:
160+
datatype["arrow_fields"] = self._arrow_fields(datatype)
161+
datatypes.append(datatype)
162+
163+
data = {
164+
"package_name": self.package_name,
165+
"schema_version": self.datamodel.schema_version,
166+
"datatypes": datatypes,
167+
}
168+
self._write_file(
169+
"ArrowMapper.h",
170+
self._eval_template("ArrowMapper.h.jinja2", data),
171+
)
172+
173+
def _arrow_fields(self, datatype):
174+
"""Create Arrow field expressions for the members and relations of a datatype"""
175+
fields = []
176+
fields.extend(
177+
self._arrow_field(member.name, self._arrow_type(member))
178+
for member in datatype["Members"]
179+
)
180+
fields.extend(
181+
self._arrow_field(member.name, f"arrow::list({self._arrow_type(member)})")
182+
for member in datatype["VectorMembers"]
183+
)
184+
fields.extend(
185+
self._arrow_field(relation.name, "objectRefType()")
186+
for relation in datatype["OneToOneRelations"]
187+
)
188+
fields.extend(
189+
self._arrow_field(relation.name, "arrow::list(objectRefType())")
190+
for relation in datatype["OneToManyRelations"]
191+
)
192+
return fields
193+
194+
def _arrow_field(self, name, type_expr, nullable=False):
195+
"""Create a C++ arrow::field expression"""
196+
nullable_arg = ", true" if nullable else ""
197+
return f'arrow::field("{name}", {type_expr}{nullable_arg})'
198+
199+
def _arrow_type(self, member):
200+
"""Map a parsed podio member to an Arrow C++ DataType expression"""
201+
if member.is_array:
202+
value_type = self._arrow_type_from_name(member.array_type)
203+
return f"arrow::fixed_size_list({value_type}, {member.array_size})"
204+
205+
return self._arrow_type_from_name(member.full_type)
206+
207+
def _arrow_type_from_name(self, type_name):
208+
"""Map a C++ type name from the datamodel to an Arrow C++ DataType expression"""
209+
type_name = type_name.removeprefix("::")
210+
if type_name in ARROW_PRIMITIVE_TYPES:
211+
return ARROW_PRIMITIVE_TYPES[type_name]
212+
213+
if type_name in self.datamodel.components:
214+
return self._arrow_struct_type(self.datamodel.components[type_name]["Members"])
215+
216+
if self.upstream_edm and type_name in self.upstream_edm.components:
217+
return self._arrow_struct_type(self.upstream_edm.components[type_name]["Members"])
218+
219+
raise DefinitionError(f"Cannot map '{type_name}' to an Arrow type")
220+
221+
def _arrow_struct_type(self, members):
222+
"""Create an Arrow struct expression for a component definition"""
223+
fields = [self._arrow_field(member.name, self._arrow_type(member)) for member in members]
224+
return "arrow::struct_({" + ", ".join(fields) + "})"
225+
125226
def do_process_interface(self, _, interface):
126227
"""Process an interface definition and generate the necessary code"""
127228
interface["include_types"] = [
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// AUTOMATICALLY GENERATED FILE - DO NOT EDIT
2+
3+
#ifndef {{ package_name.upper() }}_ARROWMAPPER_H
4+
#define {{ package_name.upper() }}_ARROWMAPPER_H
5+
6+
#include <arrow/type.h>
7+
#include <arrow/util/key_value_metadata.h>
8+
9+
#include <memory>
10+
11+
namespace {{ package_name }}::arrow_io {
12+
13+
inline std::shared_ptr<arrow::DataType> objectRefType() {
14+
return arrow::struct_({
15+
arrow::field("collectionID", arrow::uint32(), false),
16+
arrow::field("index", arrow::int32(), false),
17+
});
18+
}
19+
20+
inline std::shared_ptr<arrow::DataType> frameParametersType() {
21+
return arrow::struct_({
22+
arrow::field("int_params", arrow::map(arrow::utf8(), arrow::list(arrow::int32()))),
23+
arrow::field("float_params", arrow::map(arrow::utf8(), arrow::list(arrow::float32()))),
24+
arrow::field("double_params", arrow::map(arrow::utf8(), arrow::list(arrow::float64()))),
25+
arrow::field("string_params", arrow::map(arrow::utf8(), arrow::list(arrow::utf8()))),
26+
});
27+
}
28+
29+
inline std::shared_ptr<arrow::Schema> schema() {
30+
return arrow::schema({
31+
{% for datatype in datatypes %}
32+
arrow::field("{{ datatype.class.bare_type }}", arrow::list(arrow::struct_({
33+
{% for field in datatype.arrow_fields %}
34+
{{ field }},
35+
{% endfor %}
36+
}))),
37+
{% endfor %}
38+
arrow::field("frame_parameters", frameParametersType()),
39+
});
40+
}
41+
42+
inline std::shared_ptr<arrow::Schema>
43+
schemaWithMetadata(std::shared_ptr<const arrow::KeyValueMetadata> metadata) {
44+
return schema()->WithMetadata(std::move(metadata));
45+
}
46+
47+
} // namespace {{ package_name }}::arrow_io
48+
49+
#endif

python/templates/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ set(PODIO_TEMPLATES
1414
${CMAKE_CURRENT_LIST_DIR}/Interface.h.jinja2
1515
${CMAKE_CURRENT_LIST_DIR}/MutableObject.cc.jinja2
1616
${CMAKE_CURRENT_LIST_DIR}/MutableObject.h.jinja2
17+
${CMAKE_CURRENT_LIST_DIR}/ArrowMapper.h.jinja2
1718
${CMAKE_CURRENT_LIST_DIR}/selection.xml.jinja2
1819
${CMAKE_CURRENT_LIST_DIR}/SIOBlock.cc.jinja2
1920
${CMAKE_CURRENT_LIST_DIR}/SIOBlock.h.jinja2

tests/unittests/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ target_compile_options(unittest_podio PRIVATE -Wno-c2y-extensions)
5050
if (ENABLE_SIO)
5151
target_link_libraries(unittest_podio PRIVATE podio::podioSioIO)
5252
endif()
53+
if (ENABLE_ARROW)
54+
target_link_libraries(unittest_podio PRIVATE Arrow::arrow_shared)
55+
target_compile_definitions(unittest_podio PRIVATE PODIO_ENABLE_ARROW=1)
56+
endif()
5357

5458
# The unittests can easily be filtered and they are labelled so we can put together a
5559
# list of labels that we want to ignore

0 commit comments

Comments
 (0)