Eclipse SUMO - Simulation of Urban MObility
Loading...
Searching...
No Matches
ParquetFormatter.cpp
Go to the documentation of this file.
1/****************************************************************************/
2// Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3// Copyright (C) 2012-2026 German Aerospace Center (DLR) and others.
4// This program and the accompanying materials are made available under the
5// terms of the Eclipse Public License 2.0 which is available at
6// https://www.eclipse.org/legal/epl-2.0/
7// This Source Code may also be made available under the following Secondary
8// Licenses when the conditions for such availability set forth in the Eclipse
9// Public License 2.0 are satisfied: GNU General Public License, version 2
10// or later which is available at
11// https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13/****************************************************************************/
18// An output formatter for Parquet files
19/****************************************************************************/
20#include <config.h>
21
22#ifdef _MSC_VER
23#pragma warning(push)
24/* Disable warning about unused parameters */
25#pragma warning(disable: 4100)
26/* Disable warning about hidden function arrow::io::Writable::Write */
27#pragma warning(disable: 4266)
28/* Disable warning about padded memory layout */
29#pragma warning(disable: 4324)
30/* Disable warning about this in initializers */
31#pragma warning(disable: 4355)
32/* Disable warning about changed memory layout due to virtual base class */
33#pragma warning(disable: 4435)
34/* Disable warning about declaration hiding class member */
35#pragma warning(disable: 4458)
36/* Disable warning about implicit conversion of int to bool */
37#pragma warning(disable: 4800)
38#endif
39#include <arrow/api.h>
40#include <arrow/io/api.h>
41#include <parquet/arrow/writer.h>
42#ifdef _MSC_VER
43#pragma warning(pop)
44#endif
45
48#include "ParquetFormatter.h"
49
50
51// ===========================================================================
52// helper class definitions
53// ===========================================================================
54#ifdef _MSC_VER
55#pragma warning(push)
56/* Disable warning about hidden function arrow::io::Writable::Write */
57#pragma warning(disable: 4266)
58#endif
59class ArrowOStreamWrapper : public arrow::io::OutputStream {
60public:
61 ArrowOStreamWrapper(std::ostream& out)
62 : myOStream(out), myAmOpen(true) {}
63
64 arrow::Status Close() override {
65 myAmOpen = false;
66 return arrow::Status::OK();
67 }
68
69 arrow::Status Flush() override {
70 myOStream.flush();
71 return arrow::Status::OK();
72 }
73
74 arrow::Result<int64_t> Tell() const override {
75 return myOStream.tellp();
76 }
77
78 bool closed() const override {
79 return !myAmOpen;
80 }
81
82 arrow::Status Write(const void* data, int64_t nbytes) override {
83 if (!myAmOpen) {
84 return arrow::Status::IOError("Write on closed stream");
85 }
86 myOStream.write(reinterpret_cast<const char*>(data), nbytes);
87 if (!myOStream) {
88 return arrow::Status::IOError("Failed to write to ostream");
89 }
90 return arrow::Status::OK();
91 }
92
93private:
94 std::ostream& myOStream;
96};
97#ifdef _MSC_VER
98#pragma warning(pop)
99#endif
100
101
102// ===========================================================================
103// ParquetFormatter::Impl definition
104// ===========================================================================
106 Impl(const std::string& columnNames, const int batchSize)
107 : myHeaderFormat(columnNames), myBatchSize(batchSize) {}
108
110 const std::string myHeaderFormat;
111
113 std::vector<std::string> myFullHeader;
114
116 parquet::Compression::type myCompression = parquet::Compression::UNCOMPRESSED;
117
119 const int myBatchSize;
120
122 std::shared_ptr<arrow::Schema> mySchema = arrow::schema({});
123
125 std::unique_ptr<parquet::arrow::FileWriter> myParquetWriter;
126
128 std::vector<std::shared_ptr<arrow::ArrayBuilder> > myBuilders;
129
131 std::vector<std::pair<const std::string, int> > myXMLStack;
132
134 std::vector<std::shared_ptr<arrow::Scalar> > myValues;
135
137 int myMaxDepth = 1000;
138
140 bool myWroteHeader = false;
141
143 bool myCheckColumns = false;
144
146 bool myNeedsWrite = false;
147
149 bool myHaveRootAttrs = false;
150
153
156
157 void checkAttr(const SumoXMLAttr attr) {
158 if (myCheckColumns && myMaxDepth == (int)myXMLStack.size()) {
159 mySeenAttrs.set(attr);
160 if (!myExpectedAttrs.test(attr)) {
161 throw ProcessError(TLF("Unexpected attribute '%', this file format does not support Parquet output yet.", toString(attr)));
162 }
163 }
164 }
165
166 template <class ATTR_TYPE, class BUILDER>
167 void checkBuilder(const ATTR_TYPE& attr, const std::shared_ptr<arrow::DataType>& (*dataType)()) {
168 myNeedsWrite = true;
169 if (!myWroteHeader) {
170 std::string fieldName = toString(attr);
171 std::string prefix;
172 for (const auto& entry : myXMLStack) {
173 prefix += entry.first + "_";
174 }
175 const std::string fullHeaderName = prefix + fieldName;
176 if (myHeaderFormat == "none") {
177 fieldName = "";
178 } else if (myHeaderFormat != "plain") {
179 fieldName = myXMLStack.back().first + "_" + fieldName;
180 }
181 const auto colIt = std::find(myFullHeader.begin(), myFullHeader.end(), fullHeaderName);
182 if (colIt == myFullHeader.end()) {
183 mySchema = *mySchema->AddField(mySchema->num_fields(), arrow::field(fieldName, dataType()));
184 auto builder = std::make_shared<BUILDER>();
185 if (!myBuilders.empty()) {
186 if (myBuilders.back()->length() > 0) {
187 PARQUET_THROW_NOT_OK(builder->AppendNulls(myBuilders.back()->length()));
188 }
189 while (myValues.size() < myBuilders.size()) {
190 myValues.push_back(nullptr);
191 }
192 }
193 myBuilders.push_back(builder);
194 myFullHeader.emplace_back(fullHeaderName);
195 } else {
196 myValues.resize(std::distance(myFullHeader.begin(), colIt));
197 }
198 }
199 }
200};
201
202
203// ===========================================================================
204// member method definitions
205// ===========================================================================
206ParquetFormatter::ParquetFormatter(const std::string& columnNames, const std::string& compression, const int batchSize)
207 : OutputFormatter(OutputFormatterType::PARQUET), myImpl(std::make_unique<Impl>(columnNames, batchSize)) {
208 if (compression == "snappy") {
209 myImpl->myCompression = parquet::Compression::SNAPPY;
210 } else if (compression == "gzip") {
211 myImpl->myCompression = parquet::Compression::GZIP;
212 } else if (compression == "brotli") {
213 myImpl->myCompression = parquet::Compression::BROTLI;
214 } else if (compression == "zstd") {
215 myImpl->myCompression = parquet::Compression::ZSTD;
216 } else if (compression == "lz4") {
217 myImpl->myCompression = parquet::Compression::LZ4;
218 } else if (compression == "bz2") {
219 myImpl->myCompression = parquet::Compression::BZ2;
220 } else if (compression != "" && compression != "uncompressed") {
221 WRITE_ERRORF("Unknown compression: %", compression);
222 }
223 if (!arrow::util::Codec::IsAvailable(myImpl->myCompression)) {
224 WRITE_WARNINGF("Compression '%' not available, falling back to uncompressed.", compression);
225 myImpl->myCompression = parquet::Compression::UNCOMPRESSED;
226 }
227}
228
229
231
232
233bool
234ParquetFormatter::writeXMLHeader(std::ostream& into, const std::string& rootElement,
235 const std::map<SumoXMLAttr, std::string>& attrs, bool /* writeMetadata */,
236 bool /* includeConfig */) {
237 if (attrs.size() > 2) {
238 myImpl->myHaveRootAttrs = true;
239 openTag(into, rootElement);
240 for (const auto& a : attrs) {
241 if (a.first != SUMO_ATTR_XMLNS && a.first != SUMO_ATTR_SCHEMA_LOCATION) {
242 writeAttr(into, a.first, a.second, false, false);
243 }
244 }
245 return true;
246 }
247 return false;
248}
249
250
251void
252ParquetFormatter::openTag(std::ostream& /* into */, const std::string& xmlElement) {
253 myImpl->myXMLStack.push_back({xmlElement, (int)myImpl->myValues.size()});
254}
255
256
257void
258ParquetFormatter::openTag(std::ostream& /* into */, const SumoXMLTag& xmlElement) {
259 myImpl->myXMLStack.push_back({toString(xmlElement), (int)myImpl->myValues.size()});
260}
261
262
263bool
264ParquetFormatter::closeTag(std::ostream& into, const std::string& /* comment */) {
265 if (myImpl->myMaxDepth == 0) {
266 // the auto detection case: the first closed tag determines the depth
267 myImpl->myMaxDepth = (int)myImpl->myXMLStack.size();
268 }
269 const bool eof = myImpl->myXMLStack.empty() || (myImpl->myHaveRootAttrs && myImpl->myXMLStack.size() == 1);
270 if ((myImpl->myMaxDepth == (int)myImpl->myXMLStack.size() || eof) && !myImpl->myWroteHeader) {
271 // we are at the correct depth or the document has ended (XML stack is empty)
272 // so we should initialize the writer with the schema (if not done yet)
273 if (!myImpl->myCheckColumns) {
274 WRITE_WARNING("Column based formats are still experimental. Autodetection only works for homogeneous output.");
275 }
276 bool full = myImpl->myHeaderFormat == "full";
277 if (myImpl->myHeaderFormat == "auto") {
278 std::set<std::string> uniq;
279 for (const auto& field : myImpl->mySchema->fields()) {
280 const auto result = uniq.insert(field->name());
281 if (!result.second) {
282 full = true;
283 break;
284 }
285 }
286 }
287 if (full) {
288 arrow::FieldVector new_fields;
289 for (const auto& field : myImpl->mySchema->fields()) {
290 new_fields.push_back(field->WithName(myImpl->myFullHeader[new_fields.size()]));
291 }
292 myImpl->mySchema = arrow::schema(std::move(new_fields), myImpl->mySchema->metadata());
293 }
294 auto arrow_stream = std::make_shared<ArrowOStreamWrapper>(into);
295 std::shared_ptr<parquet::WriterProperties> props = parquet::WriterProperties::Builder().compression(myImpl->myCompression)->build();
296 myImpl->myParquetWriter = *parquet::arrow::FileWriter::Open(*myImpl->mySchema, arrow::default_memory_pool(), arrow_stream, props);
297 myImpl->myWroteHeader = true;
298 }
299 bool writeBatch = false;
300 if (myImpl->myNeedsWrite) {
301 if (myImpl->myCheckColumns && (int)myImpl->myXMLStack.size() == myImpl->myMaxDepth && myImpl->myExpectedAttrs != myImpl->mySeenAttrs) {
302 for (int i = 0; i < (int)myImpl->myExpectedAttrs.size(); ++i) {
303 if (myImpl->myExpectedAttrs.test(i) && !myImpl->mySeenAttrs.test(i)) {
304 WRITE_ERRORF("Incomplete attribute set, '%' is missing. This file format does not support Parquet output yet.",
306 }
307 }
308 }
309 int index = 0;
310 for (auto& builder : myImpl->myBuilders) {
311 const auto val = index < (int)myImpl->myValues.size() ? myImpl->myValues[index] : nullptr;
312 arrow::Status s = val == nullptr ? builder->AppendNull() : builder->AppendScalar(*val);
313 if (!s.ok()) {
314 throw ProcessError(TLF("Error writing attribute '%' (index: %, value: '%'): %",
315 myImpl->myFullHeader[index], index, val == nullptr ? "nullptr" : val->ToString(), s.ToString()));
316 }
317 index++;
318 }
319 writeBatch = myImpl->myWroteHeader && myImpl->myBuilders.back()->length() >= myImpl->myBatchSize;
320 myImpl->mySeenAttrs.reset();
321 myImpl->myNeedsWrite = false;
322 }
323 if (writeBatch || (eof && !myImpl->myBuilders.empty())) {
324 std::vector<std::shared_ptr<arrow::Array> > data;
325 for (auto& builder : myImpl->myBuilders) {
326 std::shared_ptr<arrow::Array> column;
327 PARQUET_THROW_NOT_OK(builder->Finish(&column));
328 data.push_back(column);
329 // builder.reset();
330 }
331 auto batch = arrow::RecordBatch::Make(myImpl->mySchema, data.back()->length(), data);
332 PARQUET_THROW_NOT_OK(myImpl->myParquetWriter->WriteRecordBatch(*batch));
333 }
334 if (!myImpl->myXMLStack.empty()) {
335 if ((int)myImpl->myValues.size() > myImpl->myXMLStack.back().second) {
336 myImpl->myValues.resize(myImpl->myXMLStack.back().second);
337 }
338 myImpl->myXMLStack.pop_back();
339 return true;
340 }
341 return false;
342}
343
344
345void
346ParquetFormatter::writeAttr(std::ostream& into, const SumoXMLAttr attr, const double& val, const bool isNull, const bool /* escape */) {
347 myImpl->checkAttr(attr);
348 if (attr == SUMO_ATTR_X || attr == SUMO_ATTR_Y || into.precision() > 2) {
349 myImpl->checkBuilder<SumoXMLAttr, arrow::DoubleBuilder>(attr, arrow::float64);
350 myImpl->myValues.push_back(isNull ? nullptr : std::make_shared<arrow::DoubleScalar>(val));
351 } else {
352 myImpl->checkBuilder<SumoXMLAttr, arrow::FloatBuilder>(attr, arrow::float32);
353 myImpl->myValues.push_back(isNull ? nullptr : std::make_shared<arrow::FloatScalar>((float)val));
354 }
355}
356
357
358void
359ParquetFormatter::writeAttr(std::ostream& /* into */, const SumoXMLAttr attr, const int& val, const bool isNull, const bool /* escape */) {
360 myImpl->checkAttr(attr);
361 myImpl->checkBuilder<SumoXMLAttr, arrow::Int32Builder>(attr, arrow::int32);
362 myImpl->myValues.push_back(isNull ? nullptr : std::make_shared<arrow::Int32Scalar>(val));
363}
364
365
366void
367ParquetFormatter::writeAttr(std::ostream& into, const std::string& attr, const double& val, const bool isNull, const bool /* escape */) {
368 assert(!myImpl->myCheckColumns);
369 if (into.precision() > 2) {
370 myImpl->checkBuilder<std::string, arrow::DoubleBuilder>(attr, arrow::float64);
371 myImpl->myValues.push_back(isNull ? nullptr : std::make_shared<arrow::DoubleScalar>(val));
372 } else {
373 myImpl->checkBuilder<std::string, arrow::FloatBuilder>(attr, arrow::float32);
374 myImpl->myValues.push_back(isNull ? nullptr : std::make_shared<arrow::FloatScalar>((float)val));
375 }
376}
377
378
379void
380ParquetFormatter::writeAttr(std::ostream& /* into */, const std::string& attr, const int& val, const bool isNull, const bool /* escape */) {
381 assert(!myImpl->myCheckColumns);
382 myImpl->checkBuilder<std::string, arrow::Int32Builder>(attr, arrow::int32);
383 myImpl->myValues.push_back(isNull ? nullptr : std::make_shared<arrow::Int32Scalar>(val));
384}
385
386
387void
388ParquetFormatter::writeStringAttr(const SumoXMLAttr attr, const std::string& val) {
389 myImpl->checkAttr(attr);
390 myImpl->checkBuilder<SumoXMLAttr, arrow::StringBuilder>(attr, arrow::utf8);
391 myImpl->myValues.push_back(std::make_shared<arrow::StringScalar>(val));
392}
393
394
395void
396ParquetFormatter::writeStringAttr(const std::string& attr, const std::string& val) {
397 assert(!myImpl->myCheckColumns);
398 myImpl->checkBuilder<std::string, arrow::StringBuilder>(attr, arrow::utf8);
399 myImpl->myValues.push_back(std::make_shared<arrow::StringScalar>(val));
400}
401
402
403void
405 myImpl->checkAttr(attr);
406 myImpl->checkBuilder<SumoXMLAttr, arrow::StringBuilder>(attr, arrow::utf8);
407 myImpl->myValues.push_back(nullptr);
408}
409
410
411void
412ParquetFormatter::writeNullAttr(const std::string& attr) {
413 assert(!myImpl->myCheckColumns);
414 myImpl->checkBuilder<std::string, arrow::StringBuilder>(attr, arrow::utf8);
415 myImpl->myValues.push_back(nullptr);
416}
417
418
419void
420ParquetFormatter::writeTime(std::ostream& /* into */, const SumoXMLAttr attr, const SUMOTime val) {
421 if (!gHumanReadableTime) {
422 // always float64 for machine-readable time, regardless of stream precision
423 myImpl->checkBuilder<SumoXMLAttr, arrow::DoubleBuilder>(attr, arrow::float64);
424 myImpl->myValues.push_back(std::make_shared<arrow::DoubleScalar>(STEPS2TIME(val)));
425 return;
426 }
427 writeStringAttr(attr, time2string(val));
428}
429
430
431bool
433 return myImpl->myWroteHeader;
434}
435
436
437void
439 myImpl->myExpectedAttrs = expected;
440 myImpl->myMaxDepth = depth;
441 myImpl->myCheckColumns = expected.any();
442}
443
444
445/****************************************************************************/
long long int SUMOTime
Definition GUI.h:36
#define WRITE_WARNINGF(...)
Definition MsgHandler.h:287
#define WRITE_ERRORF(...)
Definition MsgHandler.h:296
#define WRITE_WARNING(msg)
Definition MsgHandler.h:286
#define TLF(string,...)
Definition MsgHandler.h:306
OutputFormatterType
std::string time2string(SUMOTime t, bool humanReadable)
convert SUMOTime to string (independently of global format setting)
Definition SUMOTime.cpp:91
#define STEPS2TIME(x)
Definition SUMOTime.h:58
SumoXMLTag
Numbers representing SUMO-XML - element names.
std::bitset< 96 > SumoXMLAttrMask
SumoXMLAttr
Numbers representing SUMO-XML - attributes.
@ SUMO_ATTR_Y
@ SUMO_ATTR_X
@ SUMO_ATTR_XMLNS
@ SUMO_ATTR_SCHEMA_LOCATION
bool gHumanReadableTime
Definition StdDefs.cpp:31
std::string toString(const T &t, std::streamsize accuracy=gPrecision)
Definition ToString.h:49
bool closed() const override
arrow::Status Close() override
arrow::Status Flush() override
arrow::Status Write(const void *data, int64_t nbytes) override
ArrowOStreamWrapper(std::ostream &out)
arrow::Result< int64_t > Tell() const override
Abstract base class for output formatters.
ParquetFormatter(const std::string &columnNames, const std::string &compression="", const int batchSize=1000000)
Constructor.
void writeNullAttr(const SumoXMLAttr attr)
void setExpectedAttributes(const SumoXMLAttrMask &expected, const int depth) override
Which elements are expected and which maximum depth the XML tree has.
void openTag(std::ostream &into, const std::string &xmlElement) override
Keeps track of an open XML tag by adding a new element to the stack.
~ParquetFormatter() override
Destructor (out-of-line: Impl is incomplete here)
bool writeXMLHeader(std::ostream &into, const std::string &rootElement, const std::map< SumoXMLAttr, std::string > &attrs, bool, bool) override
Writes an "XML header".
bool wroteHeader() const override
Whether a complete row has been encountered and triggered writing.
std::unique_ptr< Impl > myImpl
void writeStringAttr(const SumoXMLAttr attr, const std::string &val)
non-template helpers; defined in the .cpp where arrow/parquet are available
bool closeTag(std::ostream &into, const std::string &comment="") override
Closes the most recently opened tag.
void writeAttr(std::ostream &, const SumoXMLAttr attr, const T &val, const bool isNull, const bool)
Writes a named attribute.
void writeTime(std::ostream &into, const SumoXMLAttr attr, const SUMOTime val) override
Writes a time value.
Definition json.hpp:4471
void checkBuilder(const ATTR_TYPE &attr, const std::shared_ptr< arrow::DataType > &(*dataType)())
parquet::Compression::type myCompression
the compression to use
std::vector< std::shared_ptr< arrow::Scalar > > myValues
the current attribute / column values
std::vector< std::string > myFullHeader
the column names if we write the full name
std::vector< std::shared_ptr< arrow::ArrayBuilder > > myBuilders
the content array builders for the table
std::unique_ptr< parquet::arrow::FileWriter > myParquetWriter
the output stream writer
std::shared_ptr< arrow::Schema > mySchema
the table schema
SumoXMLAttrMask mySeenAttrs
the attributes already seen (including null values)
void checkAttr(const SumoXMLAttr attr)
std::vector< std::pair< const std::string, int > > myXMLStack
The name and number of attributes in the currently open XML elements.
bool myNeedsWrite
whether there is still unwritten data
Impl(const std::string &columnNames, const int batchSize)
const int myBatchSize
the number of rows to write per batch
int myMaxDepth
the maximum depth of the XML hierarchy
SumoXMLAttrMask myExpectedAttrs
the attributes which are expected for a complete row (including null values)
bool myCheckColumns
whether the columns should be checked for completeness
bool myWroteHeader
whether the schema has been constructed completely
bool myHaveRootAttrs
whether any root attribute have been encountered
const std::string myHeaderFormat
the format to use for the column names