LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/parquet - ogrparquetwriterlayer.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 517 601 86.0 %
Date: 2026-09-09 01:17:16 Functions: 26 26 100.0 %

          Line data    Source code
       1             : /******************************************************************************
       2             :  *
       3             :  * Project:  Parquet Translator
       4             :  * Purpose:  Implements OGRParquetDriver.
       5             :  * Author:   Even Rouault, <even.rouault at spatialys.com>
       6             :  *
       7             :  ******************************************************************************
       8             :  * Copyright (c) 2022, Planet Labs
       9             :  *
      10             :  * SPDX-License-Identifier: MIT
      11             :  ****************************************************************************/
      12             : 
      13             : #ifdef STANDALONE
      14             : #include "gdal_version.h"
      15             : #else
      16             : #undef DO_NOT_DEFINE_GDAL_DATE_NAME
      17             : #include "gdal_version_full/gdal_version.h"
      18             : #endif
      19             : 
      20             : #include "ogr_parquet.h"
      21             : 
      22             : #include "../arrow_common/ograrrowwriterlayer.hpp"
      23             : 
      24             : #include "ogr_wkb.h"
      25             : 
      26             : #include <cassert>
      27             : #include <utility>
      28             : 
      29             : /************************************************************************/
      30             : /*                       OGRParquetWriterLayer()                        */
      31             : /************************************************************************/
      32             : 
      33         364 : OGRParquetWriterLayer::OGRParquetWriterLayer(
      34             :     OGRParquetWriterDataset *poDataset, arrow::MemoryPool *poMemoryPool,
      35             :     const std::shared_ptr<arrow::io::OutputStream> &poOutputStream,
      36         364 :     const char *pszLayerName)
      37             :     : OGRArrowWriterLayer(poMemoryPool, poOutputStream, pszLayerName),
      38         364 :       m_poDataset(poDataset)
      39             : {
      40         364 :     m_bWriteFieldArrowExtensionName = CPLTestBool(
      41             :         CPLGetConfigOption("OGR_PARQUET_WRITE_ARROW_EXTENSION_NAME", "NO"));
      42         364 : }
      43             : 
      44             : /************************************************************************/
      45             : /*                               Close()                                */
      46             : /************************************************************************/
      47             : 
      48         361 : bool OGRParquetWriterLayer::Close()
      49             : {
      50         361 :     if (m_poTmpGPKGLayer)
      51             :     {
      52           3 :         if (!CopyTmpGpkgLayerToFinalFile())
      53           0 :             return false;
      54             :     }
      55             : 
      56         361 :     if (m_bInitializationOK)
      57             :     {
      58         361 :         if (!FinalizeWriting())
      59           0 :             return false;
      60             :     }
      61             : 
      62         361 :     return true;
      63             : }
      64             : 
      65             : /************************************************************************/
      66             : /*                    CopyTmpGpkgLayerToFinalFile()                     */
      67             : /************************************************************************/
      68             : 
      69           3 : bool OGRParquetWriterLayer::CopyTmpGpkgLayerToFinalFile()
      70             : {
      71           3 :     if (!m_poTmpGPKGLayer)
      72             :     {
      73           0 :         return true;
      74             :     }
      75             : 
      76           3 :     CPLDebug("PARQUET", "CopyTmpGpkgLayerToFinalFile(): start...");
      77             : 
      78           3 :     VSIUnlink(m_poTmpGPKG->GetDescription());
      79             : 
      80           6 :     OGRFeature oFeat(m_poFeatureDefn);
      81             : 
      82             :     // Interval in terms of features between 2 debug progress report messages
      83           3 :     constexpr int PROGRESS_FC_INTERVAL = 100 * 1000;
      84             : 
      85             :     // First, write features without geometries
      86             :     {
      87           3 :         auto poTmpLayer = std::unique_ptr<OGRLayer>(m_poTmpGPKG->ExecuteSQL(
      88             :             "SELECT serialized_feature FROM tmp WHERE fid NOT IN (SELECT id "
      89             :             "FROM rtree_tmp_geom)",
      90           3 :             nullptr, nullptr));
      91           3 :         if (!poTmpLayer)
      92           0 :             return false;
      93        1005 :         for (const auto &poSrcFeature : poTmpLayer.get())
      94             :         {
      95        1002 :             int nBytesFeature = 0;
      96             :             const GByte *pabyFeatureData =
      97        1002 :                 poSrcFeature->GetFieldAsBinary(0, &nBytesFeature);
      98        1002 :             if (!oFeat.DeserializeFromBinary(pabyFeatureData, nBytesFeature))
      99             :             {
     100           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
     101             :                          "Cannot deserialize feature");
     102           0 :                 return false;
     103             :             }
     104        1002 :             if (OGRArrowWriterLayer::ICreateFeature(&oFeat) != OGRERR_NONE)
     105             :             {
     106           0 :                 return false;
     107             :             }
     108             : 
     109        1002 :             if ((m_nFeatureCount % PROGRESS_FC_INTERVAL) == 0)
     110             :             {
     111           0 :                 CPLDebugProgress(
     112             :                     "PARQUET",
     113             :                     "CopyTmpGpkgLayerToFinalFile(): %.02f%% progress",
     114           0 :                     100.0 * double(m_nFeatureCount) /
     115           0 :                         double(m_nTmpFeatureCount));
     116             :             }
     117             :         }
     118             : 
     119           3 :         if (!FlushFeatures())
     120             :         {
     121           0 :             return false;
     122             :         }
     123             :     }
     124             : 
     125             :     // Now walk through the GPKG RTree for features with geometries
     126             :     // Cf https://github.com/sqlite/sqlite/blob/master/ext/rtree/rtree.c
     127             :     // for the description of the content of the rtree _node table
     128           6 :     std::vector<std::pair<int64_t, int>> aNodeNoDepthPair;
     129           3 :     int nTreeDepth = 0;
     130             :     // Queue the root node
     131             :     aNodeNoDepthPair.emplace_back(
     132           3 :         std::make_pair(/* nodeNo = */ 1, /* depth = */ 0));
     133           3 :     int nCountWrittenFeaturesSinceLastFlush = 0;
     134          50 :     while (!aNodeNoDepthPair.empty())
     135             :     {
     136          47 :         const auto &oLastPair = aNodeNoDepthPair.back();
     137          47 :         const int64_t nNodeNo = oLastPair.first;
     138          47 :         const int nCurDepth = oLastPair.second;
     139             :         //CPLDebug("PARQUET", "Reading nodeNode=%d, curDepth=%d", int(nNodeNo), nCurDepth);
     140          47 :         aNodeNoDepthPair.pop_back();
     141             : 
     142          47 :         auto poRTreeLayer = std::unique_ptr<OGRLayer>(m_poTmpGPKG->ExecuteSQL(
     143             :             CPLSPrintf("SELECT data FROM rtree_tmp_geom_node WHERE nodeno "
     144             :                        "= " CPL_FRMT_GIB,
     145             :                        static_cast<GIntBig>(nNodeNo)),
     146          47 :             nullptr, nullptr));
     147          47 :         if (!poRTreeLayer)
     148             :         {
     149           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     150             :                      "Cannot read node " CPL_FRMT_GIB,
     151             :                      static_cast<GIntBig>(nNodeNo));
     152           0 :             return false;
     153             :         }
     154             :         const auto poRTreeFeature =
     155          47 :             std::unique_ptr<const OGRFeature>(poRTreeLayer->GetNextFeature());
     156          47 :         if (!poRTreeFeature)
     157             :         {
     158           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     159             :                      "Cannot read node " CPL_FRMT_GIB,
     160             :                      static_cast<GIntBig>(nNodeNo));
     161           0 :             return false;
     162             :         }
     163             : 
     164          47 :         int nNodeBytes = 0;
     165             :         const GByte *pabyNodeData =
     166          47 :             poRTreeFeature->GetFieldAsBinary(0, &nNodeBytes);
     167          47 :         constexpr int BLOB_HEADER_SIZE = 4;
     168          47 :         if (nNodeBytes < BLOB_HEADER_SIZE)
     169             :         {
     170           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     171             :                      "Not enough bytes when reading node " CPL_FRMT_GIB,
     172             :                      static_cast<GIntBig>(nNodeNo));
     173           0 :             return false;
     174             :         }
     175          47 :         if (nNodeNo == 1)
     176             :         {
     177             :             // Get the RTree depth from the root node
     178           3 :             nTreeDepth = (pabyNodeData[0] << 8) | pabyNodeData[1];
     179             :             //CPLDebug("PARQUET", "nTreeDepth = %d", nTreeDepth);
     180             :         }
     181             : 
     182          47 :         const int nCellCount = (pabyNodeData[2] << 8) | pabyNodeData[3];
     183          47 :         constexpr int SIZEOF_CELL = 24;  // int64_t + 4 float
     184          47 :         if (nNodeBytes < BLOB_HEADER_SIZE + SIZEOF_CELL * nCellCount)
     185             :         {
     186           0 :             CPLError(CE_Failure, CPLE_AppDefined,
     187             :                      "Not enough bytes when reading node " CPL_FRMT_GIB,
     188             :                      static_cast<GIntBig>(nNodeNo));
     189           0 :             return false;
     190             :         }
     191             : 
     192          47 :         size_t nOffset = BLOB_HEADER_SIZE;
     193          47 :         if (nCurDepth == nTreeDepth)
     194             :         {
     195             :             // Leaf node: it references feature IDs.
     196             : 
     197             :             // If we are about to go above m_nRowGroupSize, flush past
     198             :             // features now, to improve the spatial compacity of the row group.
     199          45 :             if (m_nRowGroupSize > nCellCount &&
     200          45 :                 nCountWrittenFeaturesSinceLastFlush + nCellCount >
     201          45 :                     m_nRowGroupSize)
     202             :             {
     203          14 :                 nCountWrittenFeaturesSinceLastFlush = 0;
     204          14 :                 if (!FlushFeatures())
     205             :                 {
     206           0 :                     return false;
     207             :                 }
     208             :             }
     209             : 
     210             :             // nCellCount shouldn't be over 51 normally, but even 65535
     211             :             // would be fine...
     212          45 :             assert(nCellCount <= 65535);
     213        1247 :             for (int i = 0; i < nCellCount; ++i)
     214             :             {
     215             :                 int64_t nFID;
     216        1202 :                 memcpy(&nFID, pabyNodeData + nOffset, sizeof(int64_t));
     217        1202 :                 CPL_MSBPTR64(&nFID);
     218             : 
     219             :                 const auto poSrcFeature = std::unique_ptr<const OGRFeature>(
     220        1202 :                     m_poTmpGPKGLayer->GetFeature(nFID));
     221        1202 :                 if (!poSrcFeature)
     222             :                 {
     223           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
     224             :                              "Cannot get feature " CPL_FRMT_GIB,
     225             :                              static_cast<GIntBig>(nFID));
     226           0 :                     return false;
     227             :                 }
     228             : 
     229        1202 :                 int nBytesFeature = 0;
     230             :                 const GByte *pabyFeatureData =
     231        1202 :                     poSrcFeature->GetFieldAsBinary(0, &nBytesFeature);
     232        1202 :                 if (!oFeat.DeserializeFromBinary(pabyFeatureData,
     233             :                                                  nBytesFeature))
     234             :                 {
     235           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
     236             :                              "Cannot deserialize feature");
     237           0 :                     return false;
     238             :                 }
     239        1202 :                 if (OGRArrowWriterLayer::ICreateFeature(&oFeat) != OGRERR_NONE)
     240             :                 {
     241           0 :                     return false;
     242             :                 }
     243             : 
     244        1202 :                 nOffset += SIZEOF_CELL;
     245             : 
     246        1202 :                 ++nCountWrittenFeaturesSinceLastFlush;
     247             : 
     248        1202 :                 if ((m_nFeatureCount % PROGRESS_FC_INTERVAL) == 0 ||
     249        1202 :                     m_nFeatureCount == m_nTmpFeatureCount / 2)
     250             :                 {
     251           2 :                     CPLDebugProgress(
     252             :                         "PARQUET",
     253             :                         "CopyTmpGpkgLayerToFinalFile(): %.02f%% progress",
     254           2 :                         100.0 * double(m_nFeatureCount) /
     255           2 :                             double(m_nTmpFeatureCount));
     256             :                 }
     257             :             }
     258             :         }
     259             :         else
     260             :         {
     261             :             // Non-leaf node: it references child nodes.
     262             : 
     263             :             // nCellCount shouldn't be over 51 normally, but even 65535
     264             :             // would be fine...
     265           2 :             assert(nCellCount <= 65535);
     266          46 :             for (int i = 0; i < nCellCount; ++i)
     267             :             {
     268             :                 int64_t nNode;
     269          44 :                 memcpy(&nNode, pabyNodeData + nOffset, sizeof(int64_t));
     270          44 :                 CPL_MSBPTR64(&nNode);
     271             :                 aNodeNoDepthPair.emplace_back(
     272          44 :                     std::make_pair(nNode, nCurDepth + 1));
     273          44 :                 nOffset += SIZEOF_CELL;
     274             :             }
     275             :         }
     276             :     }
     277             : 
     278           3 :     CPLDebug("PARQUET",
     279             :              "CopyTmpGpkgLayerToFinalFile(): 100%%, successfully finished");
     280           3 :     return true;
     281             : }
     282             : 
     283             : /************************************************************************/
     284             : /*                      IsSupportedGeometryType()                       */
     285             : /************************************************************************/
     286             : 
     287         360 : bool OGRParquetWriterLayer::IsSupportedGeometryType(
     288             :     OGRwkbGeometryType eGType) const
     289             : {
     290         360 :     const auto eFlattenType = wkbFlatten(eGType);
     291         360 :     if (!OGR_GT_HasM(eGType) && eFlattenType <= wkbGeometryCollection)
     292             :     {
     293         359 :         return true;
     294             :     }
     295             : 
     296             :     const auto osConfigOptionName =
     297           3 :         "OGR_" + GetDriverUCName() + "_ALLOW_ALL_DIMS";
     298           1 :     if (CPLTestBool(CPLGetConfigOption(osConfigOptionName.c_str(), "NO")))
     299             :     {
     300           0 :         return true;
     301             :     }
     302             : 
     303           1 :     CPLError(CE_Failure, CPLE_NotSupported,
     304             :              "Only 2D and Z geometry types are supported (unless the "
     305             :              "%s configuration option is set to YES)",
     306             :              osConfigOptionName.c_str());
     307           1 :     return false;
     308             : }
     309             : 
     310             : /************************************************************************/
     311             : /*                             SetOptions()                             */
     312             : /************************************************************************/
     313             : 
     314         364 : bool OGRParquetWriterLayer::SetOptions(
     315             :     const OGRGeomFieldDefn *poSrcGeomFieldDefn, CSLConstList papszOptions)
     316             : {
     317         364 :     m_aosCreationOptions = papszOptions;
     318             : 
     319         364 :     const char *pszWriteCoveringBBox = CSLFetchNameValueDef(
     320             :         papszOptions, "WRITE_COVERING_BBOX",
     321             :         CPLGetConfigOption("OGR_PARQUET_WRITE_COVERING_BBOX", nullptr));
     322         364 :     m_bWriteBBoxStruct =
     323         364 :         pszWriteCoveringBBox == nullptr || CPLTestBool(pszWriteCoveringBBox);
     324             : 
     325             :     m_oBBoxStructFieldName =
     326         364 :         CSLFetchNameValueDef(papszOptions, "COVERING_BBOX_NAME", "");
     327             : 
     328         364 :     if (CPLTestBool(CSLFetchNameValueDef(papszOptions, "SORT_BY_BBOX", "NO")))
     329             :     {
     330           8 :         const std::string osTmpGPKG(std::string(m_poDataset->GetDescription()) +
     331           4 :                                     ".tmp.gpkg");
     332           4 :         auto poGPKGDrv = GetGDALDriverManager()->GetDriverByName("GPKG");
     333           4 :         if (!poGPKGDrv)
     334             :         {
     335           1 :             CPLError(
     336             :                 CE_Failure, CPLE_AppDefined,
     337             :                 "Driver GPKG required for SORT_BY_BBOX layer creation option");
     338           1 :             return false;
     339             :         }
     340           3 :         m_poTmpGPKG.reset(poGPKGDrv->Create(osTmpGPKG.c_str(), 0, 0, 0,
     341             :                                             GDT_Unknown, nullptr));
     342           3 :         if (!m_poTmpGPKG)
     343           0 :             return false;
     344           3 :         m_poTmpGPKG->MarkSuppressOnClose();
     345           3 :         m_poTmpGPKGLayer = m_poTmpGPKG->CreateLayer("tmp");
     346          12 :         if (!m_poTmpGPKGLayer ||
     347             :             // Serialized feature
     348           3 :             m_poTmpGPKGLayer->CreateField(
     349           3 :                 std::make_unique<OGRFieldDefn>("serialized_feature", OFTBinary)
     350           3 :                     .get()) != OGRERR_NONE ||
     351             :             // FlushCache is needed to avoid SQLite3 errors on empty layers
     352           9 :             m_poTmpGPKG->FlushCache() != CE_None ||
     353           3 :             m_poTmpGPKGLayer->StartTransaction() != OGRERR_NONE)
     354             :         {
     355           0 :             return false;
     356             :         }
     357             :     }
     358             : 
     359             :     const char *pszGeomEncoding =
     360         363 :         CSLFetchNameValue(papszOptions, "GEOMETRY_ENCODING");
     361         363 :     m_eGeomEncoding = OGRArrowGeomEncoding::WKB;
     362         363 :     if (pszGeomEncoding)
     363             :     {
     364         211 :         if (EQUAL(pszGeomEncoding, "WKB"))
     365           7 :             m_eGeomEncoding = OGRArrowGeomEncoding::WKB;
     366         204 :         else if (EQUAL(pszGeomEncoding, "WKT"))
     367           8 :             m_eGeomEncoding = OGRArrowGeomEncoding::WKT;
     368         196 :         else if (EQUAL(pszGeomEncoding, "GEOARROW_INTERLEAVED"))
     369             :         {
     370          28 :             CPLErrorOnce(
     371             :                 CE_Warning, CPLE_AppDefined,
     372             :                 "Use of GEOMETRY_ENCODING=GEOARROW_INTERLEAVED is not "
     373             :                 "recommended. "
     374             :                 "GeoParquet 1.1 uses GEOMETRY_ENCODING=GEOARROW (struct) "
     375             :                 "instead.");
     376          28 :             m_eGeomEncoding = OGRArrowGeomEncoding::GEOARROW_FSL_GENERIC;
     377             :         }
     378         168 :         else if (EQUAL(pszGeomEncoding, "GEOARROW") ||
     379           0 :                  EQUAL(pszGeomEncoding, "GEOARROW_STRUCT"))
     380         168 :             m_eGeomEncoding = OGRArrowGeomEncoding::GEOARROW_STRUCT_GENERIC;
     381             :         else
     382             :         {
     383           0 :             CPLError(CE_Failure, CPLE_NotSupported,
     384             :                      "Unsupported GEOMETRY_ENCODING = %s", pszGeomEncoding);
     385           0 :             return false;
     386             :         }
     387             :     }
     388             : 
     389             :     const char *pszCoordPrecision =
     390         363 :         CSLFetchNameValue(papszOptions, "COORDINATE_PRECISION");
     391         363 :     if (pszCoordPrecision)
     392           0 :         m_nWKTCoordinatePrecision = atoi(pszCoordPrecision);
     393             : 
     394         363 :     m_bForceCounterClockwiseOrientation =
     395         363 :         EQUAL(CSLFetchNameValueDef(papszOptions, "POLYGON_ORIENTATION",
     396             :                                    "COUNTERCLOCKWISE"),
     397             :               "COUNTERCLOCKWISE");
     398             : 
     399             :     const auto eGType =
     400         363 :         poSrcGeomFieldDefn ? poSrcGeomFieldDefn->GetType() : wkbNone;
     401         363 :     if (poSrcGeomFieldDefn && eGType != wkbNone)
     402             :     {
     403         334 :         if (!IsSupportedGeometryType(eGType))
     404             :         {
     405           1 :             return false;
     406             :         }
     407             : 
     408         333 :         m_poFeatureDefn->SetGeomType(eGType);
     409         333 :         auto eGeomEncoding = m_eGeomEncoding;
     410         333 :         if (eGeomEncoding == OGRArrowGeomEncoding::GEOARROW_FSL_GENERIC ||
     411         305 :             eGeomEncoding == OGRArrowGeomEncoding::GEOARROW_STRUCT_GENERIC)
     412             :         {
     413         196 :             const auto eEncodingType = eGeomEncoding;
     414         196 :             eGeomEncoding = GetPreciseArrowGeomEncoding(eEncodingType, eGType);
     415         196 :             if (eGeomEncoding == eEncodingType)
     416           0 :                 return false;
     417             :         }
     418         333 :         m_aeGeomEncoding.push_back(eGeomEncoding);
     419             : 
     420         666 :         std::string osGeometryName;
     421             :         const char *pszGeometryName =
     422         333 :             CSLFetchNameValue(papszOptions, "GEOMETRY_NAME");
     423         333 :         if (pszGeometryName)
     424          16 :             osGeometryName = pszGeometryName;
     425         317 :         else if (poSrcGeomFieldDefn->GetNameRef()[0])
     426           4 :             osGeometryName = poSrcGeomFieldDefn->GetNameRef();
     427             :         else
     428         313 :             osGeometryName = "geometry";
     429         333 :         m_poFeatureDefn->GetGeomFieldDefn(0)->SetName(osGeometryName.c_str());
     430             : 
     431         333 :         const auto poSpatialRef = poSrcGeomFieldDefn->GetSpatialRef();
     432         333 :         if (poSpatialRef)
     433             :         {
     434          42 :             auto poSRS = poSpatialRef->Clone();
     435          42 :             m_poFeatureDefn->GetGeomFieldDefn(0)->SetSpatialRef(poSRS);
     436          42 :             poSRS->Release();
     437             :         }
     438             :     }
     439             : 
     440         362 :     m_osFIDColumn = CSLFetchNameValueDef(papszOptions, "FID", "");
     441             : 
     442             :     const char *pszCompression =
     443         362 :         CSLFetchNameValue(papszOptions, GDALMD_COMPRESSION);
     444         362 :     if (pszCompression == nullptr)
     445             :     {
     446        1068 :         auto oResult = arrow::util::Codec::GetCompressionType("snappy");
     447         356 :         if (oResult.ok() && arrow::util::Codec::IsAvailable(*oResult))
     448             :         {
     449         356 :             pszCompression = "SNAPPY";
     450             :         }
     451             :         else
     452             :         {
     453           0 :             pszCompression = "NONE";
     454             :         }
     455             :     }
     456             : 
     457         362 :     if (EQUAL(pszCompression, "NONE"))
     458           0 :         pszCompression = "UNCOMPRESSED";
     459             :     auto oResult = arrow::util::Codec::GetCompressionType(
     460         724 :         CPLString(pszCompression).tolower());
     461         362 :     if (!oResult.ok())
     462             :     {
     463           1 :         CPLError(CE_Failure, CPLE_NotSupported,
     464             :                  "Unrecognized compression method: %s", pszCompression);
     465           1 :         return false;
     466             :     }
     467         361 :     m_eCompression = *oResult;
     468         361 :     if (!arrow::util::Codec::IsAvailable(m_eCompression))
     469             :     {
     470           0 :         CPLError(CE_Failure, CPLE_NotSupported,
     471             :                  "Compression method %s is known, but libarrow has not "
     472             :                  "been built with support for it",
     473             :                  pszCompression);
     474           0 :         return false;
     475             :     }
     476         361 :     m_oWriterPropertiesBuilder.compression(m_eCompression);
     477             : 
     478             :     const char *pszCompressionLevel =
     479         361 :         CSLFetchNameValue(papszOptions, "COMPRESSION_LEVEL");
     480         361 :     if (pszCompressionLevel)
     481             :     {
     482           2 :         const int nCompressionLevel = atoi(pszCompressionLevel);
     483           2 :         if (nCompressionLevel != DEFAULT_COMPRESSION_LEVEL)
     484           2 :             m_oWriterPropertiesBuilder.compression_level(nCompressionLevel);
     485             :     }
     486         359 :     else if (EQUAL(pszCompression, "ZSTD"))
     487           1 :         m_oWriterPropertiesBuilder.compression_level(
     488             :             OGR_PARQUET_ZSTD_DEFAULT_COMPRESSION_LEVEL);
     489             : 
     490             :     const std::string osCreator =
     491         722 :         CSLFetchNameValueDef(papszOptions, "CREATOR", "");
     492         361 :     if (!osCreator.empty())
     493           1 :         m_oWriterPropertiesBuilder.created_by(osCreator);
     494             :     else
     495         360 :         m_oWriterPropertiesBuilder.created_by("GDAL " GDAL_RELEASE_NAME
     496             :                                               ", using " CREATED_BY_VERSION);
     497             : 
     498             :     // Undocumented option. Not clear it is useful besides unit test purposes
     499         361 :     if (!CPLTestBool(CSLFetchNameValueDef(papszOptions, "STATISTICS", "YES")))
     500           1 :         m_oWriterPropertiesBuilder.disable_statistics();
     501             : 
     502             : #if PARQUET_VERSION_MAJOR >= 12
     503             :     // Undocumented option. Not clear it is useful to disable it.
     504         361 :     if (CPLTestBool(CSLFetchNameValueDef(papszOptions, "PAGE_INDEX", "YES")))
     505         361 :         m_oWriterPropertiesBuilder.enable_write_page_index();
     506             : #endif
     507             : 
     508             :     const char *pszGeoParquetVersion =
     509         361 :         CSLFetchNameValueDef(papszOptions, "GEOPARQUET_VERSION", "1.1");
     510         361 :     if (EQUAL(pszGeoParquetVersion, "1.1") ||
     511           0 :         EQUAL(pszGeoParquetVersion, "AUTO"))
     512         361 :         m_nGeoParquetVersion = OGRGeoParquetVersion::VERSION_1_1;
     513           0 :     else if (EQUAL(pszGeoParquetVersion, "2.0"))
     514             :     {
     515             : #if ARROW_VERSION_MAJOR >= 21
     516             :         m_nGeoParquetVersion = OGRGeoParquetVersion::VERSION_2_0;
     517             :         if (pszWriteCoveringBBox == nullptr)
     518             :             m_bWriteBBoxStruct = false;
     519             : #else
     520           0 :         CPLError(CE_Failure, CPLE_NotSupported,
     521             :                  "GEOPARQUET_VERSION = 2.0 is only supported in a GDAL build "
     522             :                  "against libarrow >= 21");
     523           0 :         return false;
     524             : #endif
     525             :     }
     526             :     else
     527             :     {
     528           0 :         CPLError(CE_Failure, CPLE_NotSupported,
     529             :                  "Unrecognized GeoParquet version: %s", pszGeoParquetVersion);
     530           0 :         return false;
     531             :     }
     532             : 
     533             :     const char *pszWriteGeo =
     534         361 :         CPLGetConfigOption("OGR_PARQUET_WRITE_GEO", nullptr);
     535         361 :     m_bWriteGeoMetadata = pszWriteGeo == nullptr || CPLTestBool(pszWriteGeo);
     536             : 
     537         361 :     if (m_eGeomEncoding == OGRArrowGeomEncoding::WKB && eGType != wkbNone)
     538             :     {
     539             : #if ARROW_VERSION_MAJOR >= 21
     540             :         const char *pszUseParquetGeoTypes =
     541             :             CSLFetchNameValueDef(papszOptions, "USE_PARQUET_GEO_TYPES", "AUTO");
     542             :         if (EQUAL(pszUseParquetGeoTypes, "AUTO"))
     543             :         {
     544             :             m_bUseArrowWKBExtension =
     545             :                 (m_nGeoParquetVersion == OGRGeoParquetVersion::VERSION_2_0);
     546             :         }
     547             :         else if (EQUAL(pszUseParquetGeoTypes, "ONLY"))
     548             :         {
     549             :             m_bUseArrowWKBExtension = true;
     550             :             if (pszWriteGeo == nullptr)
     551             :                 m_bWriteGeoMetadata = false;
     552             :             if (pszWriteCoveringBBox == nullptr)
     553             :                 m_bWriteBBoxStruct = false;
     554             :         }
     555             :         else
     556             :         {
     557             :             m_bUseArrowWKBExtension = CPLTestBool(pszUseParquetGeoTypes);
     558             :             if (!m_bUseArrowWKBExtension &&
     559             :                 m_nGeoParquetVersion == OGRGeoParquetVersion::VERSION_2_0)
     560             :             {
     561             :                 CPLError(CE_Failure, CPLE_NotSupported,
     562             :                          "GEOPARQUET_VERSION = 2.0 is not compatible with "
     563             :                          "USE_PARQUET_GEO_TYPES = NO");
     564             :                 return false;
     565             :             }
     566             :         }
     567             : #else
     568         136 :         m_oWriterPropertiesBuilder.disable_statistics(
     569         408 :             parquet::schema::ColumnPath::FromDotString(
     570         136 :                 m_poFeatureDefn->GetGeomFieldDefn(0)->GetNameRef()));
     571             : #endif
     572             :     }
     573             : 
     574             :     const char *pszRowGroupSize =
     575         361 :         CSLFetchNameValue(papszOptions, "ROW_GROUP_SIZE");
     576         361 :     if (pszRowGroupSize)
     577             :     {
     578          19 :         auto nRowGroupSize = static_cast<int64_t>(atoll(pszRowGroupSize));
     579          19 :         if (nRowGroupSize > 0)
     580             :         {
     581          19 :             if (nRowGroupSize > INT_MAX)
     582           0 :                 nRowGroupSize = INT_MAX;
     583          19 :             m_nRowGroupSize = nRowGroupSize;
     584             :         }
     585             :     }
     586             : 
     587         361 :     m_bEdgesSpherical = EQUAL(
     588             :         CSLFetchNameValueDef(papszOptions, "EDGES", "PLANAR"), "SPHERICAL");
     589             : 
     590         361 :     m_bInitializationOK = true;
     591         361 :     return true;
     592             : }
     593             : 
     594             : /************************************************************************/
     595             : /*                          CloseFileWriter()                           */
     596             : /************************************************************************/
     597             : 
     598         361 : bool OGRParquetWriterLayer::CloseFileWriter()
     599             : {
     600         722 :     auto status = m_poFileWriter->Close();
     601         361 :     if (!status.ok())
     602             :     {
     603           0 :         CPLError(CE_Failure, CPLE_AppDefined,
     604             :                  "FileWriter::Close() failed with %s",
     605           0 :                  status.message().c_str());
     606             :     }
     607         722 :     return status.ok();
     608             : }
     609             : 
     610             : /************************************************************************/
     611             : /*                           GetGeoMetadata()                           */
     612             : /************************************************************************/
     613             : 
     614         361 : std::string OGRParquetWriterLayer::GetGeoMetadata() const
     615             : {
     616             :     // Just for unit testing purposes
     617             :     const char *pszGeoMetadata =
     618         361 :         CPLGetConfigOption("OGR_PARQUET_GEO_METADATA", nullptr);
     619         361 :     if (pszGeoMetadata)
     620          16 :         return pszGeoMetadata;
     621             : 
     622         345 :     if (m_poFeatureDefn->GetGeomFieldCount() != 0 && m_bWriteGeoMetadata)
     623             :     {
     624         650 :         CPLJSONObject oRoot;
     625         325 :         oRoot.Add("version",
     626         325 :                   m_nGeoParquetVersion == OGRGeoParquetVersion::VERSION_1_1
     627             :                       ? "1.1.0"
     628             :                       : "2.0.0");
     629         325 :         oRoot.Add("primary_column",
     630         325 :                   m_poFeatureDefn->GetGeomFieldDefn(0)->GetNameRef());
     631         650 :         CPLJSONObject oColumns;
     632         325 :         oRoot.Add("columns", oColumns);
     633         667 :         for (int i = 0; i < m_poFeatureDefn->GetGeomFieldCount(); ++i)
     634             :         {
     635         342 :             const auto poGeomFieldDefn = m_poFeatureDefn->GetGeomFieldDefn(i);
     636         684 :             CPLJSONObject oColumn;
     637         342 :             oColumns.Add(poGeomFieldDefn->GetNameRef(), oColumn);
     638         342 :             oColumn.Add("encoding",
     639         342 :                         GetGeomEncodingAsString(m_aeGeomEncoding[i], true));
     640             : 
     641         342 :             if (CPLTestBool(CPLGetConfigOption("OGR_PARQUET_WRITE_CRS", "YES")))
     642             :             {
     643         341 :                 const auto poSRS = poGeomFieldDefn->GetSpatialRef();
     644         341 :                 if (poSRS)
     645             :                 {
     646          82 :                     OGRSpatialReference oSRSIdentified(IdentifyCRS(poSRS));
     647             : 
     648          41 :                     const char *pszAuthName = oSRSIdentified.GetAuthorityName();
     649          41 :                     const char *pszAuthCode = oSRSIdentified.GetAuthorityCode();
     650             : 
     651          41 :                     bool bOmitCRS = false;
     652          41 :                     if (pszAuthName != nullptr && pszAuthCode != nullptr &&
     653          40 :                         ((EQUAL(pszAuthName, "EPSG") &&
     654          37 :                           EQUAL(pszAuthCode, "4326")) ||
     655          22 :                          (EQUAL(pszAuthName, "OGC") &&
     656           3 :                           EQUAL(pszAuthCode, "CRS84"))))
     657             :                     {
     658             :                         // To make things less confusing for non-geo-aware
     659             :                         // consumers, omit EPSG:4326 / OGC:CRS84 CRS by default
     660          21 :                         bOmitCRS = CPLTestBool(CPLGetConfigOption(
     661             :                             "OGR_PARQUET_CRS_OMIT_IF_WGS84", "YES"));
     662             :                     }
     663             : 
     664          41 :                     if (bOmitCRS)
     665             :                     {
     666             :                         // do nothing
     667             :                     }
     668          20 :                     else if (EQUAL(CPLGetConfigOption(
     669             :                                        "OGR_PARQUET_CRS_ENCODING", "PROJJSON"),
     670             :                                    "PROJJSON"))
     671             :                     {
     672             :                         // CRS encoded as PROJJSON for GeoParquet >= 0.4.0
     673          20 :                         char *pszPROJJSON = nullptr;
     674          20 :                         oSRSIdentified.exportToPROJJSON(&pszPROJJSON, nullptr);
     675          40 :                         CPLJSONDocument oCRSDoc;
     676          20 :                         CPL_IGNORE_RET_VAL(oCRSDoc.LoadMemory(pszPROJJSON));
     677          20 :                         CPLFree(pszPROJJSON);
     678          20 :                         CPLJSONObject oCRSRoot = oCRSDoc.GetRoot();
     679          20 :                         RemoveIDFromMemberOfEnsembles(oCRSRoot);
     680          20 :                         oColumn.Add("crs", oCRSRoot);
     681             :                     }
     682             :                     else
     683             :                     {
     684             :                         // WKT was used in GeoParquet <= 0.3.0
     685           0 :                         const char *const apszOptions[] = {
     686             :                             "FORMAT=WKT2_2019", "MULTILINE=NO", nullptr};
     687           0 :                         char *pszWKT = nullptr;
     688           0 :                         oSRSIdentified.exportToWkt(&pszWKT, apszOptions);
     689           0 :                         if (pszWKT)
     690           0 :                             oColumn.Add("crs", pszWKT);
     691           0 :                         CPLFree(pszWKT);
     692             :                     }
     693             : 
     694          41 :                     const double dfCoordEpoch = poSRS->GetCoordinateEpoch();
     695          41 :                     if (dfCoordEpoch > 0)
     696           2 :                         oColumn.Add("epoch", dfCoordEpoch);
     697             :                 }
     698             :                 else
     699             :                 {
     700         300 :                     oColumn.AddNull("crs");
     701             :                 }
     702             :             }
     703             : 
     704         342 :             if (m_bEdgesSpherical)
     705             :             {
     706           3 :                 oColumn.Add("edges", "spherical");
     707             :             }
     708             : 
     709         643 :             if (m_aoEnvelopes[i].IsInit() &&
     710         301 :                 CPLTestBool(
     711             :                     CPLGetConfigOption("OGR_PARQUET_WRITE_BBOX", "YES")))
     712             :             {
     713         301 :                 bool bHasZ = false;
     714         537 :                 for (const auto eGeomType : m_oSetWrittenGeometryTypes[i])
     715             :                 {
     716         343 :                     bHasZ = CPL_TO_BOOL(OGR_GT_HasZ(eGeomType));
     717         343 :                     if (bHasZ)
     718         107 :                         break;
     719             :                 }
     720         301 :                 CPLJSONArray oBBOX;
     721         301 :                 oBBOX.Add(m_aoEnvelopes[i].MinX);
     722         301 :                 oBBOX.Add(m_aoEnvelopes[i].MinY);
     723         301 :                 if (bHasZ)
     724         107 :                     oBBOX.Add(m_aoEnvelopes[i].MinZ);
     725         301 :                 oBBOX.Add(m_aoEnvelopes[i].MaxX);
     726         301 :                 oBBOX.Add(m_aoEnvelopes[i].MaxY);
     727         301 :                 if (bHasZ)
     728         107 :                     oBBOX.Add(m_aoEnvelopes[i].MaxZ);
     729         301 :                 oColumn.Add("bbox", oBBOX);
     730             :             }
     731             : 
     732             :             // Bounding box column definition
     733         614 :             if (m_bWriteBBoxStruct &&
     734         272 :                 CPLTestBool(CPLGetConfigOption(
     735             :                     "OGR_PARQUET_WRITE_COVERING_BBOX_IN_METADATA", "YES")))
     736             :             {
     737         544 :                 CPLJSONObject oCovering;
     738         272 :                 oColumn.Add("covering", oCovering);
     739         544 :                 CPLJSONObject oBBOX;
     740         272 :                 oCovering.Add("bbox", oBBOX);
     741             :                 const auto AddComponent =
     742        3264 :                     [this, i, &oBBOX](const char *pszComponent)
     743             :                 {
     744        1088 :                     CPLJSONArray oArray;
     745        1088 :                     oArray.Add(m_apoFieldsBBOX[i]->name());
     746        1088 :                     oArray.Add(pszComponent);
     747        1088 :                     oBBOX.Add(pszComponent, oArray);
     748        1088 :                 };
     749         272 :                 AddComponent("xmin");
     750         272 :                 AddComponent("ymin");
     751         272 :                 AddComponent("xmax");
     752         272 :                 AddComponent("ymax");
     753             :             }
     754             : 
     755         359 :             const auto GetStringGeometryType = [](OGRwkbGeometryType eType)
     756             :             {
     757         359 :                 const auto eFlattenType = wkbFlatten(eType);
     758         359 :                 std::string osType = "Unknown";
     759         359 :                 if (wkbPoint == eFlattenType)
     760          85 :                     osType = "Point";
     761         274 :                 else if (wkbLineString == eFlattenType)
     762          42 :                     osType = "LineString";
     763         232 :                 else if (wkbPolygon == eFlattenType)
     764          70 :                     osType = "Polygon";
     765         162 :                 else if (wkbMultiPoint == eFlattenType)
     766          34 :                     osType = "MultiPoint";
     767         128 :                 else if (wkbMultiLineString == eFlattenType)
     768          37 :                     osType = "MultiLineString";
     769          91 :                 else if (wkbMultiPolygon == eFlattenType)
     770          86 :                     osType = "MultiPolygon";
     771           5 :                 else if (wkbGeometryCollection == eFlattenType)
     772           5 :                     osType = "GeometryCollection";
     773         359 :                 if (osType != "Unknown")
     774             :                 {
     775             :                     // M and ZM not supported officially currently, but it
     776             :                     // doesn't hurt to anticipate
     777         359 :                     if (OGR_GT_HasZ(eType) && OGR_GT_HasM(eType))
     778           8 :                         osType += " ZM";
     779         351 :                     else if (OGR_GT_HasZ(eType))
     780         115 :                         osType += " Z";
     781         236 :                     else if (OGR_GT_HasM(eType))
     782           8 :                         osType += " M";
     783             :                 }
     784         359 :                 return osType;
     785             :             };
     786             : 
     787         342 :             if (m_bForceCounterClockwiseOrientation)
     788         341 :                 oColumn.Add("orientation", "counterclockwise");
     789             : 
     790         342 :             CPLJSONArray oArray;
     791         701 :             for (const auto eType : m_oSetWrittenGeometryTypes[i])
     792             :             {
     793         359 :                 oArray.Add(GetStringGeometryType(eType));
     794             :             }
     795         342 :             oColumn.Add("geometry_types", oArray);
     796             :         }
     797             : 
     798         325 :         return oRoot.Format(CPLJSONObject::PrettyFormat::Plain);
     799             :     }
     800          20 :     return std::string();
     801             : }
     802             : 
     803             : /************************************************************************/
     804             : /*                 PerformStepsBeforeFinalFlushGroup()                  */
     805             : /************************************************************************/
     806             : 
     807         361 : void OGRParquetWriterLayer::PerformStepsBeforeFinalFlushGroup()
     808             : {
     809         361 :     if (m_poKeyValueMetadata)
     810             :     {
     811         722 :         std::string osGeoMetadata = GetGeoMetadata();
     812         722 :         auto poTmpSchema = m_poSchema;
     813         361 :         if (!osGeoMetadata.empty())
     814             :         {
     815             :             // HACK: it would be good for Arrow to provide a clean way to alter
     816             :             // key value metadata before finalizing.
     817             :             // We need to write metadata at end to write the bounding box.
     818         341 :             const_cast<arrow::KeyValueMetadata *>(m_poKeyValueMetadata.get())
     819         341 :                 ->Append("geo", osGeoMetadata);
     820             : 
     821         341 :             auto kvMetadata = poTmpSchema->metadata()
     822          15 :                                   ? poTmpSchema->metadata()->Copy()
     823         356 :                                   : std::make_shared<arrow::KeyValueMetadata>();
     824         341 :             kvMetadata->Append("geo", std::move(osGeoMetadata));
     825         341 :             poTmpSchema = poTmpSchema->WithMetadata(kvMetadata);
     826             :         }
     827             : 
     828         361 :         if (CPLTestBool(
     829             :                 CPLGetConfigOption("OGR_PARQUET_WRITE_ARROW_SCHEMA", "YES")))
     830             :         {
     831             :             auto status =
     832         722 :                 ::arrow::ipc::SerializeSchema(*poTmpSchema, m_poMemoryPool);
     833         361 :             if (status.ok())
     834             :             {
     835             :                 // The serialized schema is not UTF-8, which is required for
     836             :                 // Thrift
     837         722 :                 const std::string schema_as_string = (*status)->ToString();
     838             :                 std::string schema_base64 =
     839         361 :                     ::arrow::util::base64_encode(schema_as_string);
     840         361 :                 static const std::string kArrowSchemaKey = "ARROW:schema";
     841             :                 const_cast<arrow::KeyValueMetadata *>(
     842         361 :                     m_poKeyValueMetadata.get())
     843         361 :                     ->Append(kArrowSchemaKey, std::move(schema_base64));
     844             :             }
     845             :         }
     846             : 
     847             :         // Put GDAL metadata into a gdal:metadata domain
     848         722 :         CPLJSONObject oMultiMetadata;
     849         361 :         bool bHasMultiMetadata = false;
     850         369 :         auto &l_oMDMD = oMDMD.GetDomainList() && *(oMDMD.GetDomainList())
     851         369 :                             ? oMDMD
     852         353 :                             : m_poDataset->GetMultiDomainMetadata();
     853         371 :         for (CSLConstList papszDomainIter = l_oMDMD.GetDomainList();
     854         371 :              papszDomainIter && *papszDomainIter; ++papszDomainIter)
     855             :         {
     856          10 :             const char *pszDomain = *papszDomainIter;
     857          10 :             CSLConstList papszMD = l_oMDMD.GetMetadata(pszDomain);
     858          10 :             if (STARTS_WITH(pszDomain, "json:") && papszMD && papszMD[0])
     859             :             {
     860           1 :                 CPLJSONDocument oDoc;
     861           1 :                 if (oDoc.LoadMemory(papszMD[0]))
     862             :                 {
     863           1 :                     bHasMultiMetadata = true;
     864           1 :                     oMultiMetadata.Add(pszDomain, oDoc.GetRoot());
     865           1 :                     continue;
     866           0 :                 }
     867             :             }
     868           9 :             else if (STARTS_WITH(pszDomain, "xml:") && papszMD && papszMD[0])
     869             :             {
     870           1 :                 bHasMultiMetadata = true;
     871           1 :                 oMultiMetadata.Add(pszDomain, papszMD[0]);
     872           1 :                 continue;
     873             :             }
     874          16 :             CPLJSONObject oMetadata;
     875           8 :             bool bHasMetadata = false;
     876          16 :             for (CSLConstList papszMDIter = papszMD;
     877          16 :                  papszMDIter && *papszMDIter; ++papszMDIter)
     878             :             {
     879           8 :                 char *pszKey = nullptr;
     880           8 :                 const char *pszValue = CPLParseNameValue(*papszMDIter, &pszKey);
     881           8 :                 if (pszKey && pszValue)
     882             :                 {
     883           8 :                     bHasMetadata = true;
     884           8 :                     bHasMultiMetadata = true;
     885           8 :                     oMetadata.Add(pszKey, pszValue);
     886             :                 }
     887           8 :                 CPLFree(pszKey);
     888             :             }
     889           8 :             if (bHasMetadata)
     890           8 :                 oMultiMetadata.Add(pszDomain, oMetadata);
     891             :         }
     892         361 :         if (bHasMultiMetadata)
     893             :         {
     894           8 :             const_cast<arrow::KeyValueMetadata *>(m_poKeyValueMetadata.get())
     895           8 :                 ->Append(
     896             :                     "gdal:metadata",
     897          16 :                     oMultiMetadata.Format(CPLJSONObject::PrettyFormat::Plain));
     898             :         }
     899             : 
     900         361 :         if (!m_aosCreationOptions.empty())
     901             :         {
     902         508 :             CPLJSONObject oCreationOptions;
     903         254 :             bool bEmpty = true;
     904        1120 :             for (const auto &[key, value] :
     905        1374 :                  cpl::IterateNameValue(m_aosCreationOptions))
     906             :             {
     907         560 :                 if (!EQUAL(key, "FID") && !EQUAL(key, "GEOMETRY_NAME") &&
     908         521 :                     !EQUAL(key, "EDGES"))
     909             :                 {
     910         517 :                     bEmpty = false;
     911         517 :                     oCreationOptions.Add(key, value);
     912             :                 }
     913             :             }
     914         254 :             if (!bEmpty)
     915             :             {
     916             :                 const_cast<arrow::KeyValueMetadata *>(
     917         240 :                     m_poKeyValueMetadata.get())
     918         240 :                     ->Append("gdal:creation-options",
     919         480 :                              oCreationOptions.Format(
     920             :                                  CPLJSONObject::PrettyFormat::Plain));
     921             :             }
     922             :         }
     923             :     }
     924         361 : }
     925             : 
     926             : /************************************************************************/
     927             : /*                                Open()                                */
     928             : /************************************************************************/
     929             : 
     930             : // Same as parquet::arrow::FileWriter::Open(), except we also
     931             : // return KeyValueMetadata
     932             : static arrow::Status
     933         361 : Open(const ::arrow::Schema &schema, ::arrow::MemoryPool *pool,
     934             :      std::shared_ptr<::arrow::io::OutputStream> sink,
     935             :      std::shared_ptr<parquet::WriterProperties> properties,
     936             :      std::shared_ptr<parquet::ArrowWriterProperties> arrow_properties,
     937             :      std::unique_ptr<parquet::arrow::FileWriter> *writer,
     938             :      std::shared_ptr<const arrow::KeyValueMetadata> *outMetadata)
     939             : {
     940         361 :     std::shared_ptr<parquet::SchemaDescriptor> parquet_schema;
     941         722 :     RETURN_NOT_OK(parquet::arrow::ToParquetSchema(
     942             :         &schema, *properties, *arrow_properties, &parquet_schema));
     943             : 
     944             :     auto schema_node = std::static_pointer_cast<parquet::schema::GroupNode>(
     945         722 :         parquet_schema->schema_root());
     946             : 
     947         361 :     auto metadata = schema.metadata()
     948          20 :                         ? schema.metadata()->Copy()
     949         742 :                         : std::make_shared<arrow::KeyValueMetadata>();
     950         361 :     *outMetadata = metadata;
     951             : 
     952         361 :     std::unique_ptr<parquet::ParquetFileWriter> base_writer;
     953         361 :     PARQUET_CATCH_NOT_OK(base_writer = parquet::ParquetFileWriter::Open(
     954             :                              std::move(sink), std::move(schema_node),
     955             :                              std::move(properties), metadata));
     956             : 
     957         361 :     auto schema_ptr = std::make_shared<::arrow::Schema>(schema);
     958             :     return parquet::arrow::FileWriter::Make(
     959         722 :         pool, std::move(base_writer), std::move(schema_ptr),
     960        1083 :         std::move(arrow_properties), writer);
     961             : }
     962             : 
     963             : /************************************************************************/
     964             : /*                            CreateSchema()                            */
     965             : /************************************************************************/
     966             : 
     967         361 : void OGRParquetWriterLayer::CreateSchema()
     968             : {
     969         361 :     CreateSchemaCommon();
     970         361 : }
     971             : 
     972             : /************************************************************************/
     973             : /*                          CreateGeomField()                           */
     974             : /************************************************************************/
     975             : 
     976          27 : OGRErr OGRParquetWriterLayer::CreateGeomField(const OGRGeomFieldDefn *poField,
     977             :                                               int bApproxOK)
     978             : {
     979          27 :     OGRErr eErr = OGRArrowWriterLayer::CreateGeomField(poField, bApproxOK);
     980          53 :     if (eErr == OGRERR_NONE &&
     981          26 :         m_aeGeomEncoding.back() == OGRArrowGeomEncoding::WKB
     982             : #if ARROW_VERSION_MAJOR < 21
     983             :         // Geostatistics in Arrow 21 do not support geographic type for now
     984          53 :         && m_bEdgesSpherical
     985             : #endif
     986             :     )
     987             :     {
     988           0 :         m_oWriterPropertiesBuilder.disable_statistics(
     989           0 :             parquet::schema::ColumnPath::FromDotString(
     990           0 :                 m_poFeatureDefn
     991           0 :                     ->GetGeomFieldDefn(m_poFeatureDefn->GetGeomFieldCount() - 1)
     992             :                     ->GetNameRef()));
     993             :     }
     994          27 :     return eErr;
     995             : }
     996             : 
     997             : /************************************************************************/
     998             : /*                            CreateWriter()                            */
     999             : /************************************************************************/
    1000             : 
    1001         361 : void OGRParquetWriterLayer::CreateWriter()
    1002             : {
    1003         361 :     CPLAssert(m_poFileWriter == nullptr);
    1004             : 
    1005         361 :     if (m_poSchema == nullptr)
    1006             :     {
    1007          43 :         CreateSchema();
    1008             :     }
    1009             :     else
    1010             :     {
    1011         318 :         FinalizeSchema();
    1012             :     }
    1013             : 
    1014             :     auto arrowWriterProperties =
    1015         361 :         parquet::ArrowWriterProperties::Builder().store_schema()->build();
    1016        1083 :     CPL_IGNORE_RET_VAL(Open(*m_poSchema, m_poMemoryPool, m_poOutputStream,
    1017         722 :                             m_oWriterPropertiesBuilder.build(),
    1018         361 :                             std::move(arrowWriterProperties), &m_poFileWriter,
    1019             :                             &m_poKeyValueMetadata));
    1020         361 : }
    1021             : 
    1022             : /************************************************************************/
    1023             : /*                           ICreateFeature()                           */
    1024             : /************************************************************************/
    1025             : 
    1026        3390 : OGRErr OGRParquetWriterLayer::ICreateFeature(OGRFeature *poFeature)
    1027             : {
    1028             :     // If not using SORT_BY_BBOX=YES layer creation option, we can directly
    1029             :     // write features to the final Parquet file
    1030        3390 :     if (!m_poTmpGPKGLayer)
    1031        1186 :         return OGRArrowWriterLayer::ICreateFeature(poFeature);
    1032             : 
    1033             :     // SORT_BY_BBOX=YES case: we write for now a serialized version of poFeature
    1034             :     // in a temporary GeoPackage file.
    1035             : 
    1036        2204 :     GIntBig nFID = poFeature->GetFID();
    1037        2204 :     if (!m_osFIDColumn.empty() && nFID == OGRNullFID)
    1038             :     {
    1039        1102 :         nFID = m_nTmpFeatureCount;
    1040        1102 :         poFeature->SetFID(nFID);
    1041             :     }
    1042        2204 :     ++m_nTmpFeatureCount;
    1043             : 
    1044        4408 :     std::vector<GByte> abyBuffer;
    1045             :     // Serialize the source feature as a single array of bytes to preserve it
    1046             :     // fully
    1047        2204 :     if (!poFeature->SerializeToBinary(abyBuffer))
    1048             :     {
    1049           0 :         return OGRERR_FAILURE;
    1050             :     }
    1051             : 
    1052             :     // SQLite3 limitation: a row must fit in slightly less than 1 GB.
    1053        2204 :     constexpr int SOME_MARGIN = 128;
    1054        2204 :     if (abyBuffer.size() > 1024 * 1024 * 1024 - SOME_MARGIN)
    1055             :     {
    1056           0 :         CPLError(CE_Failure, CPLE_NotSupported,
    1057             :                  "Features larger than 1 GB are not supported");
    1058           0 :         return OGRERR_FAILURE;
    1059             :     }
    1060             : 
    1061        4408 :     OGRFeature oFeat(m_poTmpGPKGLayer->GetLayerDefn());
    1062        2204 :     oFeat.SetFID(nFID);
    1063        2204 :     oFeat.SetField(0, static_cast<int>(abyBuffer.size()), abyBuffer.data());
    1064        2204 :     const auto poSrcGeom = poFeature->GetGeometryRef();
    1065        2204 :     if (poSrcGeom && !poSrcGeom->IsEmpty())
    1066             :     {
    1067             :         // For the purpose of building an RTree, just use the bounding box of
    1068             :         // the geometry as the geometry.
    1069        1202 :         OGREnvelope sEnvelope;
    1070        1202 :         poSrcGeom->getEnvelope(&sEnvelope);
    1071        2404 :         auto poPoly = std::make_unique<OGRPolygon>();
    1072        2404 :         auto poLR = std::make_unique<OGRLinearRing>();
    1073        1202 :         poLR->addPoint(sEnvelope.MinX, sEnvelope.MinY);
    1074        1202 :         poLR->addPoint(sEnvelope.MinX, sEnvelope.MaxY);
    1075        1202 :         poLR->addPoint(sEnvelope.MaxX, sEnvelope.MaxY);
    1076        1202 :         poLR->addPoint(sEnvelope.MaxX, sEnvelope.MinY);
    1077        1202 :         poLR->addPoint(sEnvelope.MinX, sEnvelope.MinY);
    1078        1202 :         poPoly->addRingDirectly(poLR.release());
    1079        1202 :         oFeat.SetGeometryDirectly(poPoly.release());
    1080             :     }
    1081        2204 :     return m_poTmpGPKGLayer->CreateFeature(&oFeat);
    1082             : }
    1083             : 
    1084             : /************************************************************************/
    1085             : /*                             FlushGroup()                             */
    1086             : /************************************************************************/
    1087             : 
    1088         333 : bool OGRParquetWriterLayer::FlushGroup()
    1089             : {
    1090             : #if PARQUET_VERSION_MAJOR >= 20
    1091             :     auto status = m_poFileWriter->NewRowGroup();
    1092             : #else
    1093         666 :     auto status = m_poFileWriter->NewRowGroup(m_apoBuilders[0]->length());
    1094             : #endif
    1095         333 :     if (!status.ok())
    1096             :     {
    1097           0 :         CPLError(CE_Failure, CPLE_AppDefined, "NewRowGroup() failed with %s",
    1098           0 :                  status.message().c_str());
    1099           0 :         ClearArrayBuilers();
    1100           0 :         return false;
    1101             :     }
    1102             : 
    1103         333 :     auto ret = WriteArrays(
    1104        1291 :         [this](const std::shared_ptr<arrow::Field> &field,
    1105        1291 :                const std::shared_ptr<arrow::Array> &array)
    1106             :         {
    1107        2582 :             auto l_status = m_poFileWriter->WriteColumnChunk(*array);
    1108        1291 :             if (!l_status.ok())
    1109             :             {
    1110           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1111             :                          "WriteColumnChunk() failed for field %s: %s",
    1112           0 :                          field->name().c_str(), l_status.message().c_str());
    1113           0 :                 return false;
    1114             :             }
    1115        1291 :             return true;
    1116             :         });
    1117             : 
    1118         333 :     ClearArrayBuilers();
    1119         333 :     return ret;
    1120             : }
    1121             : 
    1122             : /************************************************************************/
    1123             : /*                   FixupWKBGeometryBeforeWriting()                    */
    1124             : /************************************************************************/
    1125             : 
    1126          51 : void OGRParquetWriterLayer::FixupWKBGeometryBeforeWriting(GByte *pabyWkb,
    1127             :                                                           size_t nLen)
    1128             : {
    1129          51 :     if (!m_bForceCounterClockwiseOrientation)
    1130           0 :         return;
    1131             : 
    1132          51 :     OGRWKBFixupCounterClockWiseExternalRing(pabyWkb, nLen);
    1133             : }
    1134             : 
    1135             : /************************************************************************/
    1136             : /*                     FixupGeometryBeforeWriting()                     */
    1137             : /************************************************************************/
    1138             : 
    1139        1429 : void OGRParquetWriterLayer::FixupGeometryBeforeWriting(OGRGeometry *poGeom)
    1140             : {
    1141        1429 :     if (!m_bForceCounterClockwiseOrientation)
    1142           3 :         return;
    1143             : 
    1144        1426 :     const auto eFlattenType = wkbFlatten(poGeom->getGeometryType());
    1145             :     // Polygon rings MUST follow the right-hand rule for orientation
    1146             :     // (counterclockwise external rings, clockwise internal rings)
    1147        1426 :     if (eFlattenType == wkbPolygon)
    1148             :     {
    1149          74 :         bool bFirstRing = true;
    1150         151 :         for (auto poRing : poGeom->toPolygon())
    1151             :         {
    1152          85 :             if ((bFirstRing && poRing->isClockwise()) ||
    1153           8 :                 (!bFirstRing && !poRing->isClockwise()))
    1154             :             {
    1155          72 :                 poRing->reversePoints();
    1156             :             }
    1157          77 :             bFirstRing = false;
    1158             :         }
    1159             :     }
    1160        1352 :     else if (eFlattenType == wkbMultiPolygon ||
    1161             :              eFlattenType == wkbGeometryCollection)
    1162             :     {
    1163         135 :         for (auto poSubGeom : poGeom->toGeometryCollection())
    1164             :         {
    1165          71 :             FixupGeometryBeforeWriting(poSubGeom);
    1166             :         }
    1167             :     }
    1168             : }
    1169             : 
    1170             : /************************************************************************/
    1171             : /*                          WriteArrowBatch()                           */
    1172             : /************************************************************************/
    1173             : 
    1174             : #if PARQUET_VERSION_MAJOR > 10
    1175             : inline bool
    1176          25 : OGRParquetWriterLayer::WriteArrowBatch(const struct ArrowSchema *schema,
    1177             :                                        struct ArrowArray *array,
    1178             :                                        CSLConstList papszOptions)
    1179             : {
    1180          25 :     if (m_poTmpGPKGLayer)
    1181             :     {
    1182             :         // When using SORT_BY_BBOX=YES option, we can't directly write the
    1183             :         // input array, because we need to sort features. Hence we fallback
    1184             :         // to the OGRLayer base implementation, which will ultimately call
    1185             :         // OGRParquetWriterLayer::ICreateFeature()
    1186           0 :         return OGRLayer::WriteArrowBatch(schema, array, papszOptions);
    1187             :     }
    1188             : 
    1189          50 :     return WriteArrowBatchInternal(
    1190             :         schema, array, papszOptions,
    1191          50 :         [this](const std::shared_ptr<arrow::RecordBatch> &poBatch)
    1192             :         {
    1193          50 :             auto status = m_poFileWriter->NewBufferedRowGroup();
    1194          25 :             if (!status.ok())
    1195             :             {
    1196           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1197             :                          "NewBufferedRowGroup() failed with %s",
    1198           0 :                          status.message().c_str());
    1199           0 :                 return false;
    1200             :             }
    1201             : 
    1202          25 :             status = m_poFileWriter->WriteRecordBatch(*poBatch);
    1203          25 :             if (!status.ok())
    1204             :             {
    1205           0 :                 CPLError(CE_Failure, CPLE_AppDefined,
    1206             :                          "WriteRecordBatch() failed: %s",
    1207           0 :                          status.message().c_str());
    1208           0 :                 return false;
    1209             :             }
    1210             : 
    1211          25 :             return true;
    1212          25 :         });
    1213             : }
    1214             : #endif
    1215             : 
    1216             : /************************************************************************/
    1217             : /*                           TestCapability()                           */
    1218             : /************************************************************************/
    1219             : 
    1220         694 : inline bool OGRParquetWriterLayer::TestCapability(const char *pszCap) const
    1221             : {
    1222             : #if PARQUET_VERSION_MAJOR <= 10
    1223             :     if (EQUAL(pszCap, OLCFastWriteArrowBatch))
    1224             :         return false;
    1225             : #endif
    1226             : 
    1227         694 :     if (m_poTmpGPKGLayer && EQUAL(pszCap, OLCFastWriteArrowBatch))
    1228             :     {
    1229             :         // When using SORT_BY_BBOX=YES option, we can't directly write the
    1230             :         // input array, because we need to sort features. So this is not
    1231             :         // fast
    1232           1 :         return false;
    1233             :     }
    1234             : 
    1235         693 :     return OGRArrowWriterLayer::TestCapability(pszCap);
    1236             : }
    1237             : 
    1238             : /************************************************************************/
    1239             : /*                     CreateFieldFromArrowSchema()                     */
    1240             : /************************************************************************/
    1241             : 
    1242             : #if PARQUET_VERSION_MAJOR > 10
    1243         479 : bool OGRParquetWriterLayer::CreateFieldFromArrowSchema(
    1244             :     const struct ArrowSchema *schema, CSLConstList papszOptions)
    1245             : {
    1246         479 :     if (m_poTmpGPKGLayer)
    1247             :     {
    1248             :         // When using SORT_BY_BBOX=YES option, we can't directly write the
    1249             :         // input array, because we need to sort features. But this process
    1250             :         // only supports the base Arrow types supported by
    1251             :         // OGRLayer::WriteArrowBatch()
    1252           0 :         return OGRLayer::CreateFieldFromArrowSchema(schema, papszOptions);
    1253             :     }
    1254             : 
    1255         479 :     return OGRArrowWriterLayer::CreateFieldFromArrowSchema(schema,
    1256         479 :                                                            papszOptions);
    1257             : }
    1258             : #endif
    1259             : 
    1260             : /************************************************************************/
    1261             : /*                       IsArrowSchemaSupported()                       */
    1262             : /************************************************************************/
    1263             : 
    1264             : #if PARQUET_VERSION_MAJOR > 10
    1265        1095 : bool OGRParquetWriterLayer::IsArrowSchemaSupported(
    1266             :     const struct ArrowSchema *schema, CSLConstList papszOptions,
    1267             :     std::string &osErrorMsg) const
    1268             : {
    1269        1095 :     if (m_poTmpGPKGLayer)
    1270             :     {
    1271             :         // When using SORT_BY_BBOX=YES option, we can't directly write the
    1272             :         // input array, because we need to sort features. But this process
    1273             :         // only supports the base Arrow types supported by
    1274             :         // OGRLayer::WriteArrowBatch()
    1275           0 :         return OGRLayer::IsArrowSchemaSupported(schema, papszOptions,
    1276           0 :                                                 osErrorMsg);
    1277             :     }
    1278             : 
    1279        1095 :     if (schema->format[0] == 'e' && schema->format[1] == 0)
    1280             :     {
    1281           1 :         osErrorMsg = "float16 not supported";
    1282           1 :         return false;
    1283             :     }
    1284        1094 :     if (schema->format[0] == 'v' && schema->format[1] == 'u')
    1285             :     {
    1286           1 :         osErrorMsg = "StringView not supported";
    1287           1 :         return false;
    1288             :     }
    1289        1093 :     if (schema->format[0] == 'v' && schema->format[1] == 'z')
    1290             :     {
    1291           1 :         osErrorMsg = "BinaryView not supported";
    1292           1 :         return false;
    1293             :     }
    1294        1092 :     if (schema->format[0] == '+' && schema->format[1] == 'v')
    1295             :     {
    1296           0 :         if (schema->format[2] == 'l')
    1297             :         {
    1298           0 :             osErrorMsg = "ListView not supported";
    1299           0 :             return false;
    1300             :         }
    1301           0 :         else if (schema->format[2] == 'L')
    1302             :         {
    1303           0 :             osErrorMsg = "LargeListView not supported";
    1304           0 :             return false;
    1305             :         }
    1306             :     }
    1307        2169 :     for (int64_t i = 0; i < schema->n_children; ++i)
    1308             :     {
    1309        1080 :         if (!IsArrowSchemaSupported(schema->children[i], papszOptions,
    1310             :                                     osErrorMsg))
    1311             :         {
    1312           3 :             return false;
    1313             :         }
    1314             :     }
    1315        1089 :     return true;
    1316             : }
    1317             : #endif
    1318             : 
    1319             : /************************************************************************/
    1320             : /*                            SetMetadata()                             */
    1321             : /************************************************************************/
    1322             : 
    1323          13 : CPLErr OGRParquetWriterLayer::SetMetadata(CSLConstList papszMetadata,
    1324             :                                           const char *pszDomain)
    1325             : {
    1326          13 :     if (!pszDomain || !EQUAL(pszDomain, "SHAPEFILE"))
    1327             :     {
    1328           8 :         return OGRLayer::SetMetadata(papszMetadata, pszDomain);
    1329             :     }
    1330           5 :     return CE_None;
    1331             : }
    1332             : 
    1333             : /************************************************************************/
    1334             : /*                             GetDataset()                             */
    1335             : /************************************************************************/
    1336             : 
    1337          26 : GDALDataset *OGRParquetWriterLayer::GetDataset()
    1338             : {
    1339          26 :     return m_poDataset;
    1340             : }

Generated by: LCOV version 1.14