LCOV - code coverage report
Current view: top level - ogr/ogrsf_frmts/parquet - ogrparquetlayer.cpp (source / functions) Hit Total Coverage
Test: gdal_filtered.info Lines: 1344 1447 92.9 %
Date: 2026-04-15 22:10:00 Functions: 57 57 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             : #include "cpl_json.h"
      14             : #include "cpl_time.h"
      15             : #include "cpl_multiproc.h"
      16             : #include "gdal_pam.h"
      17             : #include "ogrsf_frmts.h"
      18             : #include "ogr_p.h"
      19             : #include "gdal_thread_pool.h"
      20             : 
      21             : #include <algorithm>
      22             : #include <cinttypes>
      23             : #include <cmath>
      24             : #include <limits>
      25             : #include <map>
      26             : #include <set>
      27             : #include <utility>
      28             : 
      29             : #include "ogr_parquet.h"
      30             : 
      31             : #include "../arrow_common/ograrrowlayer.hpp"
      32             : #include "../arrow_common/ograrrowdataset.hpp"
      33             : 
      34             : /************************************************************************/
      35             : /*                        OGRParquetLayerBase()                         */
      36             : /************************************************************************/
      37             : 
      38        1418 : OGRParquetLayerBase::OGRParquetLayerBase(OGRParquetDataset *poDS,
      39             :                                          const char *pszLayerName,
      40        1418 :                                          CSLConstList papszOpenOptions)
      41             :     : OGRArrowLayer(poDS, pszLayerName,
      42        1418 :                     CPLTestBool(CSLFetchNameValueDef(
      43             :                         papszOpenOptions, "LISTS_AS_STRING_JSON", "NO"))),
      44             :       m_poDS(poDS),
      45             :       m_aosGeomPossibleNames(CSLTokenizeString2(
      46             :           CSLFetchNameValueDef(papszOpenOptions, "GEOM_POSSIBLE_NAMES",
      47             :                                "geometry,wkb_geometry,wkt_geometry"),
      48             :           ",", 0)),
      49        2836 :       m_osCRS(CSLFetchNameValueDef(papszOpenOptions, "CRS", ""))
      50             : {
      51        1418 : }
      52             : 
      53             : /************************************************************************/
      54             : /*                             GetDataset()                             */
      55             : /************************************************************************/
      56             : 
      57          27 : GDALDataset *OGRParquetLayerBase::GetDataset()
      58             : {
      59          27 :     return m_poDS;
      60             : }
      61             : 
      62             : /************************************************************************/
      63             : /*                            ResetReading()                            */
      64             : /************************************************************************/
      65             : 
      66        8062 : void OGRParquetLayerBase::ResetReading()
      67             : {
      68        8062 :     if (m_iRecordBatch != 0)
      69             :     {
      70        7594 :         m_poRecordBatchReader.reset();
      71             :     }
      72        8062 :     OGRArrowLayer::ResetReading();
      73        8062 : }
      74             : 
      75             : /************************************************************************/
      76             : /*                      InvalidateCachedBatches()                       */
      77             : /************************************************************************/
      78             : 
      79        2201 : void OGRParquetLayerBase::InvalidateCachedBatches()
      80             : {
      81        2201 :     m_iRecordBatch = -1;
      82        2201 :     ResetReading();
      83        2201 : }
      84             : 
      85             : /************************************************************************/
      86             : /*                          LoadGeoMetadata()                           */
      87             : /************************************************************************/
      88             : 
      89        1418 : void OGRParquetLayerBase::LoadGeoMetadata(
      90             :     const std::shared_ptr<const arrow::KeyValueMetadata> &kv_metadata)
      91             : {
      92        1418 :     if (kv_metadata && kv_metadata->Contains("geo"))
      93             :     {
      94        2688 :         auto geo = kv_metadata->Get("geo");
      95        1344 :         if (geo.ok())
      96             :         {
      97        1344 :             CPLDebug("PARQUET", "geo = %s", geo->c_str());
      98        2688 :             CPLJSONDocument oDoc;
      99        1344 :             if (oDoc.LoadMemory(*geo))
     100             :             {
     101        2686 :                 auto oRoot = oDoc.GetRoot();
     102        4029 :                 const auto osVersion = oRoot.GetString("version");
     103        3281 :                 if (osVersion != "0.1.0" && osVersion != "0.2.0" &&
     104        2906 :                     osVersion != "0.3.0" && osVersion != "0.4.0" &&
     105        2902 :                     osVersion != "1.0.0-beta.1" && osVersion != "1.0.0-rc.1" &&
     106        3279 :                     osVersion != "1.0.0" && osVersion != "1.1.0")
     107             :                 {
     108           1 :                     CPLDebug(
     109             :                         "PARQUET",
     110             :                         "version = %s not explicitly handled by the driver",
     111             :                         osVersion.c_str());
     112             :                 }
     113             : 
     114        4029 :                 auto oColumns = oRoot.GetObj("columns");
     115        1343 :                 if (oColumns.IsValid())
     116             :                 {
     117        2701 :                     for (const auto &oColumn : oColumns.GetChildren())
     118             :                     {
     119        1359 :                         m_oMapGeometryColumns[oColumn.GetName()] = oColumn;
     120             :                     }
     121             :                 }
     122             :             }
     123             :             else
     124             :             {
     125           1 :                 CPLError(CE_Warning, CPLE_AppDefined,
     126             :                          "Cannot parse 'geo' metadata");
     127             :             }
     128             :         }
     129             :     }
     130        1418 : }
     131             : 
     132             : /************************************************************************/
     133             : /*                    ParseGeometryColumnCovering()                     */
     134             : /************************************************************************/
     135             : 
     136             : //! Parse bounding box column definition
     137             : /*static */
     138        2708 : bool OGRParquetLayerBase::ParseGeometryColumnCovering(
     139             :     const CPLJSONObject &oJSONDef, std::string &osBBOXColumn,
     140             :     std::string &osXMin, std::string &osYMin, std::string &osXMax,
     141             :     std::string &osYMax)
     142             : {
     143        8124 :     const auto oCovering = oJSONDef["covering"];
     144        4096 :     if (oCovering.IsValid() &&
     145        1388 :         oCovering.GetType() == CPLJSONObject::Type::Object)
     146             :     {
     147        2776 :         const auto oBBOX = oCovering["bbox"];
     148        1388 :         if (oBBOX.IsValid() && oBBOX.GetType() == CPLJSONObject::Type::Object)
     149             :         {
     150        2776 :             const auto oXMin = oBBOX["xmin"];
     151        2776 :             const auto oYMin = oBBOX["ymin"];
     152        2776 :             const auto oXMax = oBBOX["xmax"];
     153        2776 :             const auto oYMax = oBBOX["ymax"];
     154        2776 :             if (oXMin.IsValid() && oYMin.IsValid() && oXMax.IsValid() &&
     155        1388 :                 oYMax.IsValid() &&
     156        1388 :                 oXMin.GetType() == CPLJSONObject::Type::Array &&
     157        1388 :                 oYMin.GetType() == CPLJSONObject::Type::Array &&
     158        4164 :                 oXMax.GetType() == CPLJSONObject::Type::Array &&
     159        1388 :                 oYMax.GetType() == CPLJSONObject::Type::Array)
     160             :             {
     161        1388 :                 const auto osXMinArray = oXMin.ToArray();
     162        1388 :                 const auto osYMinArray = oYMin.ToArray();
     163        1388 :                 const auto osXMaxArray = oXMax.ToArray();
     164        1388 :                 const auto osYMaxArray = oYMax.ToArray();
     165        1388 :                 if (osXMinArray.Size() == 2 && osYMinArray.Size() == 2 &&
     166        1388 :                     osXMaxArray.Size() == 2 && osYMaxArray.Size() == 2 &&
     167        2776 :                     osXMinArray[0].GetType() == CPLJSONObject::Type::String &&
     168        2776 :                     osXMinArray[1].GetType() == CPLJSONObject::Type::String &&
     169        2776 :                     osYMinArray[0].GetType() == CPLJSONObject::Type::String &&
     170        2776 :                     osYMinArray[1].GetType() == CPLJSONObject::Type::String &&
     171        2776 :                     osXMaxArray[0].GetType() == CPLJSONObject::Type::String &&
     172        2776 :                     osXMaxArray[1].GetType() == CPLJSONObject::Type::String &&
     173        2776 :                     osYMaxArray[0].GetType() == CPLJSONObject::Type::String &&
     174        4164 :                     osYMaxArray[1].GetType() == CPLJSONObject::Type::String &&
     175        4164 :                     osXMinArray[0].ToString() == osYMinArray[0].ToString() &&
     176        5552 :                     osXMinArray[0].ToString() == osXMaxArray[0].ToString() &&
     177        2776 :                     osXMinArray[0].ToString() == osYMaxArray[0].ToString())
     178             :                 {
     179        1388 :                     osBBOXColumn = osXMinArray[0].ToString();
     180        1388 :                     osXMin = osXMinArray[1].ToString();
     181        1388 :                     osYMin = osYMinArray[1].ToString();
     182        1388 :                     osXMax = osXMaxArray[1].ToString();
     183        1388 :                     osYMax = osYMaxArray[1].ToString();
     184        1388 :                     return true;
     185             :                 }
     186             :             }
     187             :         }
     188             :     }
     189        1320 :     return false;
     190             : }
     191             : 
     192             : /************************************************************************/
     193             : /*            DealWithArrow21GeometryGeographyNativeTypes()             */
     194             : /************************************************************************/
     195             : 
     196             : #if PARQUET_VERSION_MAJOR >= 21
     197             : 
     198             : /** Returns true if the passed field is detected to be a native Geometry/Geography
     199             :  * column as introduced in libarrow >= 21.
     200             :  *
     201             :  * In that case, m_aeGeomEncoding, m_poFeatureDefn and m_anMapGeomFieldIndexToArrowColumn
     202             :  * are updated with the new geometry column.
     203             :  */
     204             : bool OGRParquetLayerBase::DealWithArrow21GeometryGeographyNativeTypes(
     205             :     int iFieldIdx, const std::shared_ptr<arrow::Field> &field,
     206             :     const parquet::ColumnDescriptor *parquetColumn,
     207             :     const parquet::FileMetaData *fileMetadata, int iColumn)
     208             : {
     209             :     std::shared_ptr<arrow::DataType> fieldType = field->type();
     210             :     auto fieldTypeId = fieldType->id();
     211             : 
     212             :     // Arrow >= 21 GEOMETRY/GEOGRAPHY logical type are seen as an extension
     213             :     // because OGRParquetDriverOpen registers OGRGeoArrowWkbExtensionType
     214             :     // as dealing with geoarrow.wkb
     215             :     if (fieldTypeId != arrow::Type::EXTENSION)
     216             :         return false;
     217             : 
     218             :     auto extensionType =
     219             :         cpl::down_cast<arrow::ExtensionType *>(fieldType.get());
     220             :     if (extensionType->extension_name() != EXTENSION_NAME_GEOARROW_WKB)
     221             :         return false;
     222             : 
     223             :     fieldTypeId = extensionType->storage_type()->id();
     224             :     if (fieldTypeId != arrow::Type::BINARY &&
     225             :         fieldTypeId != arrow::Type::LARGE_BINARY)
     226             :     {
     227             :         return false;
     228             :     }
     229             : 
     230             :     const auto arrowWkb =
     231             :         dynamic_cast<const OGRGeoArrowWkbExtensionType *>(extensionType);
     232             : #ifdef DEBUG
     233             :     if (arrowWkb)
     234             :     {
     235             :         CPLDebug("PARQUET", "arrowWkb = '%s'", arrowWkb->Serialize().c_str());
     236             :     }
     237             : #endif
     238             : 
     239             :     OGRwkbGeometryType eGeomType = wkbUnknown;
     240             :     bool bSkipRowGroups = false;
     241             : 
     242             :     std::string crs(m_osCRS);
     243             :     if (parquetColumn && crs.empty())
     244             :     {
     245             :         const auto &logicalType = parquetColumn->logical_type();
     246             :         if (logicalType->is_geometry())
     247             :         {
     248             :             crs = static_cast<const parquet::GeometryLogicalType *>(
     249             :                       logicalType.get())
     250             :                       ->crs();
     251             :             if (crs.empty())
     252             :                 crs = "EPSG:4326";
     253             :             CPLDebugOnly("PARQUET", "GeometryLogicalType crs=%s", crs.c_str());
     254             :         }
     255             :         else if (logicalType->is_geography())
     256             :         {
     257             :             const auto *geographyType =
     258             :                 static_cast<const parquet::GeographyLogicalType *>(
     259             :                     logicalType.get());
     260             :             crs = geographyType->crs();
     261             :             if (crs.empty())
     262             :                 crs = "EPSG:4326";
     263             : 
     264             :             SetMetadataItem(
     265             :                 "EDGES", CPLString(std::string(geographyType->algorithm_name()))
     266             :                              .toupper());
     267             :             CPLDebugOnly("PARQUET", "GeographyLogicalType crs=%s", crs.c_str());
     268             :         }
     269             :         else
     270             :         {
     271             :             CPLDebug("PARQUET", "geoarrow.wkb column is neither a "
     272             :                                 "geometry or geography one");
     273             : 
     274             :             return false;
     275             :         }
     276             : 
     277             :         // Cf https://github.com/apache/parquet-format/blob/master/Geospatial.md#crs-customization
     278             :         // "projjson: PROJJSON, identifier is the name of a table property or a file property where the projjson string is stored."
     279             :         // Here the property is interpreted as the key of a file metadata (as done in libarrow)
     280             :         constexpr const char *PROJJSON_PREFIX = "projjson:";
     281             :         if (cpl::starts_with(crs, PROJJSON_PREFIX) && fileMetadata)
     282             :         {
     283             :             auto projjson_value = fileMetadata->key_value_metadata()->Get(
     284             :                 crs.substr(strlen(PROJJSON_PREFIX)));
     285             :             if (projjson_value.ok())
     286             :             {
     287             :                 crs = *projjson_value;
     288             :             }
     289             :             else
     290             :             {
     291             :                 CPLDebug("PARQUET", "Cannot find file metadata for %s",
     292             :                          crs.c_str());
     293             :             }
     294             :         }
     295             :     }
     296             :     else if (!parquetColumn && arrowWkb)
     297             :     {
     298             :         // For a OGRParquetDatasetLayer for example
     299             :         const std::string arrowWkbMetadata = arrowWkb->Serialize();
     300             :         if (arrowWkbMetadata.empty() || arrowWkbMetadata == "{}")
     301             :         {
     302             :             crs = "EPSG:4326";
     303             :         }
     304             :         else if (arrowWkbMetadata[0] == '{')
     305             :         {
     306             :             CPLJSONDocument oDoc;
     307             :             if (oDoc.LoadMemory(arrowWkbMetadata))
     308             :             {
     309             :                 auto jCrs = oDoc.GetRoot()["crs"];
     310             :                 if (jCrs.GetType() == CPLJSONObject::Type::Object)
     311             :                 {
     312             :                     crs = jCrs.Format(CPLJSONObject::PrettyFormat::Plain);
     313             :                 }
     314             :                 else if (jCrs.GetType() == CPLJSONObject::Type::String)
     315             :                 {
     316             :                     crs = jCrs.ToString();
     317             :                 }
     318             :                 if (oDoc.GetRoot()["edges"].ToString() == "spherical")
     319             :                 {
     320             :                     SetMetadataItem("EDGES", "SPHERICAL");
     321             :                 }
     322             :             }
     323             :         }
     324             :     }
     325             : 
     326             :     bool bGeomTypeInvalid = false;
     327             :     bool bHasMulti = false;
     328             :     bool bHasZ = false;
     329             :     bool bHasM = false;
     330             :     bool bFirst = true;
     331             :     OGRwkbGeometryType eFirstType = wkbUnknown;
     332             :     OGRwkbGeometryType eFirstTypeCollection = wkbUnknown;
     333             :     const auto numRowGroups = fileMetadata ? fileMetadata->num_row_groups() : 0;
     334             :     bool bEnvelopeValid = true;
     335             :     OGREnvelope sEnvelope;
     336             :     bool bEnvelope3DValid = true;
     337             :     OGREnvelope3D sEnvelope3D;
     338             :     for (int iRowGroup = 0; !bSkipRowGroups && iRowGroup < numRowGroups;
     339             :          ++iRowGroup)
     340             :     {
     341             :         const auto columnChunk =
     342             :             fileMetadata->RowGroup(iRowGroup)->ColumnChunk(iColumn);
     343             :         if (auto geostats = columnChunk->geo_statistics())
     344             :         {
     345             :             double dfMinX = std::numeric_limits<double>::quiet_NaN();
     346             :             double dfMinY = std::numeric_limits<double>::quiet_NaN();
     347             :             double dfMinZ = std::numeric_limits<double>::quiet_NaN();
     348             :             double dfMaxX = std::numeric_limits<double>::quiet_NaN();
     349             :             double dfMaxY = std::numeric_limits<double>::quiet_NaN();
     350             :             double dfMaxZ = std::numeric_limits<double>::quiet_NaN();
     351             :             if (bEnvelopeValid && geostats->dimension_valid()[0] &&
     352             :                 geostats->dimension_valid()[1])
     353             :             {
     354             :                 dfMinX = geostats->lower_bound()[0];
     355             :                 dfMaxX = geostats->upper_bound()[0];
     356             :                 dfMinY = geostats->lower_bound()[1];
     357             :                 dfMaxY = geostats->upper_bound()[1];
     358             : 
     359             :                 // Deal as best as we can with wrap around bounding box
     360             :                 if (dfMinX > dfMaxX && std::fabs(dfMinX) <= 180 &&
     361             :                     std::fabs(dfMaxX) <= 180)
     362             :                 {
     363             :                     dfMinX = -180;
     364             :                     dfMaxX = 180;
     365             :                 }
     366             : 
     367             :                 if (std::isfinite(dfMinX) && std::isfinite(dfMaxX) &&
     368             :                     std::isfinite(dfMinY) && std::isfinite(dfMaxY))
     369             :                 {
     370             :                     sEnvelope.Merge(dfMinX, dfMinY);
     371             :                     sEnvelope.Merge(dfMaxX, dfMaxY);
     372             :                     if (bEnvelope3DValid && geostats->dimension_valid()[2])
     373             :                     {
     374             :                         dfMinZ = geostats->lower_bound()[2];
     375             :                         dfMaxZ = geostats->upper_bound()[2];
     376             :                         if (std::isfinite(dfMinZ) && std::isfinite(dfMaxZ))
     377             :                         {
     378             :                             sEnvelope3D.Merge(dfMinX, dfMinY, dfMinZ);
     379             :                             sEnvelope3D.Merge(dfMaxX, dfMaxY, dfMaxZ);
     380             :                         }
     381             :                     }
     382             :                 }
     383             :             }
     384             : 
     385             :             bEnvelopeValid = bEnvelopeValid && std::isfinite(dfMinX) &&
     386             :                              std::isfinite(dfMaxX) && std::isfinite(dfMinY) &&
     387             :                              std::isfinite(dfMaxY);
     388             : 
     389             :             bEnvelope3DValid = bEnvelope3DValid && std::isfinite(dfMinZ) &&
     390             :                                std::isfinite(dfMaxZ);
     391             : 
     392             :             if (auto geometry_types = geostats->geometry_types())
     393             :             {
     394             :                 const auto PromoteToCollection = [](OGRwkbGeometryType eType)
     395             :                 {
     396             :                     if (eType == wkbPoint)
     397             :                         return wkbMultiPoint;
     398             :                     if (eType == wkbLineString)
     399             :                         return wkbMultiLineString;
     400             :                     if (eType == wkbPolygon)
     401             :                         return wkbMultiPolygon;
     402             :                     return eType;
     403             :                 };
     404             : 
     405             :                 for (int nGeomType : *geometry_types)
     406             :                 {
     407             :                     OGRwkbGeometryType eThisGeom = wkbUnknown;
     408             :                     if ((nGeomType > 0 && nGeomType <= 17) ||
     409             :                         (nGeomType > 2000 && nGeomType <= 2017) ||
     410             :                         (nGeomType > 3000 && nGeomType <= 3017))
     411             :                     {
     412             :                         eThisGeom = static_cast<OGRwkbGeometryType>(nGeomType);
     413             :                     }
     414             :                     else if (nGeomType > 1000 && nGeomType <= 1017)
     415             :                     {
     416             :                         eThisGeom = OGR_GT_SetZ(
     417             :                             static_cast<OGRwkbGeometryType>(nGeomType - 1000));
     418             :                         ;
     419             :                     }
     420             :                     else
     421             :                     {
     422             :                         CPLDebug("PARQUET", "Unknown geometry type: %d",
     423             :                                  nGeomType);
     424             :                         bGeomTypeInvalid = true;
     425             :                         break;
     426             :                     }
     427             :                     if (bFirst)
     428             :                     {
     429             :                         bFirst = false;
     430             :                         eFirstType = eThisGeom;
     431             :                         eFirstTypeCollection = PromoteToCollection(eFirstType);
     432             :                     }
     433             :                     else if (PromoteToCollection(OGR_GT_Flatten(eThisGeom)) !=
     434             :                              eFirstTypeCollection)
     435             :                     {
     436             :                         bGeomTypeInvalid = true;
     437             :                         break;
     438             :                     }
     439             :                     bHasZ |= OGR_GT_HasZ(eThisGeom) != FALSE;
     440             :                     bHasM |= OGR_GT_HasM(eThisGeom) != FALSE;
     441             :                     bHasMulti |=
     442             :                         (PromoteToCollection(OGR_GT_Flatten(eThisGeom)) ==
     443             :                          OGR_GT_Flatten(eThisGeom));
     444             :                 }
     445             :             }
     446             :         }
     447             :         else
     448             :         {
     449             :             bEnvelopeValid = false;
     450             :             bEnvelope3DValid = false;
     451             :             bGeomTypeInvalid = true;
     452             :         }
     453             :     }
     454             : 
     455             :     if (bEnvelopeValid && sEnvelope.IsInit())
     456             :     {
     457             :         CPLDebug("PARQUET", "Got bounding box from geo_statistics");
     458             :         m_geoStatsWithBBOXAvailable.insert(
     459             :             m_poFeatureDefn->GetGeomFieldCount());
     460             :         m_oMapExtents[m_poFeatureDefn->GetGeomFieldCount()] =
     461             :             std::move(sEnvelope);
     462             : 
     463             :         if (bEnvelope3DValid && sEnvelope3D.IsInit())
     464             :         {
     465             :             CPLDebug("PARQUET", "Got bounding box 3D from geo_statistics");
     466             :             m_oMapExtents3D[m_poFeatureDefn->GetGeomFieldCount()] =
     467             :                 std::move(sEnvelope3D);
     468             :         }
     469             :     }
     470             : 
     471             :     if (!bSkipRowGroups && !bGeomTypeInvalid)
     472             :     {
     473             :         if (eFirstTypeCollection == wkbMultiPoint ||
     474             :             eFirstTypeCollection == wkbMultiPolygon ||
     475             :             eFirstTypeCollection == wkbMultiLineString)
     476             :         {
     477             :             if (bHasMulti)
     478             :                 eGeomType =
     479             :                     OGR_GT_SetModifier(eFirstTypeCollection, bHasZ, bHasM);
     480             :             else
     481             :                 eGeomType = OGR_GT_SetModifier(eFirstType, bHasZ, bHasM);
     482             :         }
     483             :     }
     484             : 
     485             :     OGRGeomFieldDefn oField(field->name().c_str(), eGeomType);
     486             :     oField.SetNullable(field->nullable());
     487             : 
     488             :     if (!crs.empty())
     489             :     {
     490             :         // Cf https://github.com/apache/parquet-format/blob/master/Geospatial.md#crs-customization
     491             :         // "srid: Spatial reference identifier, identifier is the SRID itself.."
     492             :         constexpr const char *SRID_PREFIX = "srid:";
     493             :         if (cpl::starts_with(crs, SRID_PREFIX))
     494             :         {
     495             :             // When getting the value from the GeometryLogicalType::crs() method
     496             :             crs = crs.substr(strlen(SRID_PREFIX));
     497             :         }
     498             :         if (CPLGetValueType(crs.c_str()) == CPL_VALUE_INTEGER)
     499             :         {
     500             :             // Getting here from above if, or if reading the ArrowWkb
     501             :             // metadata directly (typically from a OGRParquetDatasetLayer)
     502             : 
     503             :             // Assumes a SRID code is an EPSG code...
     504             :             crs = std::string("EPSG:") + crs;
     505             :         }
     506             : 
     507             :         auto poSRS = OGRSpatialReferenceRefCountedPtr::makeInstance();
     508             :         poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     509             :         if (poSRS->SetFromUserInput(
     510             :                 crs.c_str(),
     511             :                 OGRSpatialReference::SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
     512             :             OGRERR_NONE)
     513             :         {
     514             :             const char *pszAuthName = poSRS->GetAuthorityName(nullptr);
     515             :             const char *pszAuthCode = poSRS->GetAuthorityCode(nullptr);
     516             :             if (pszAuthName && pszAuthCode && EQUAL(pszAuthName, "OGC") &&
     517             :                 EQUAL(pszAuthCode, "CRS84"))
     518             :                 poSRS->importFromEPSG(4326);
     519             :             oField.SetSpatialRef(poSRS.get());
     520             :         }
     521             :     }
     522             : 
     523             :     m_aeGeomEncoding.push_back(OGRArrowGeomEncoding::WKB);
     524             :     m_poFeatureDefn->AddGeomFieldDefn(&oField);
     525             :     m_anMapGeomFieldIndexToArrowColumn.push_back(iFieldIdx);
     526             : 
     527             :     return true;
     528             : }
     529             : 
     530             : #endif
     531             : 
     532             : /************************************************************************/
     533             : /*                       DealWithGeometryColumn()                       */
     534             : /************************************************************************/
     535             : 
     536       32716 : bool OGRParquetLayerBase::DealWithGeometryColumn(
     537             :     int iFieldIdx, const std::shared_ptr<arrow::Field> &field,
     538             :     std::function<OGRwkbGeometryType(void)> computeGeometryTypeFun,
     539             :     [[maybe_unused]] const parquet::ColumnDescriptor *parquetColumn,
     540             :     [[maybe_unused]] const parquet::FileMetaData *fileMetadata,
     541             :     [[maybe_unused]] int iColumn)
     542             : {
     543             : #if PARQUET_VERSION_MAJOR >= 21
     544             :     if (DealWithArrow21GeometryGeographyNativeTypes(
     545             :             iFieldIdx, field, parquetColumn, fileMetadata, iColumn))
     546             :         return true;
     547             : #endif
     548             : 
     549       65432 :     const auto &field_kv_metadata = field->metadata();
     550       65432 :     std::string osExtensionName;
     551       32716 :     if (field_kv_metadata)
     552             :     {
     553             : #ifdef DEBUG
     554         116 :         const auto keyValueSortedPairs = field_kv_metadata->sorted_pairs();
     555          58 :         if (!keyValueSortedPairs.empty())
     556             :         {
     557           9 :             CPLDebug("PARQUET",
     558           9 :                      "Metadata for field '%s':", field->name().c_str());
     559          19 :             for (const auto &keyValue : keyValueSortedPairs)
     560             :             {
     561          10 :                 CPLDebug("PARQUET", "  '%s' = '%s'", keyValue.first.c_str(),
     562             :                          keyValue.second.c_str());
     563             :             }
     564             :         }
     565             : #endif
     566         116 :         auto extension_name = field_kv_metadata->Get(ARROW_EXTENSION_NAME_KEY);
     567          58 :         if (extension_name.ok())
     568             :         {
     569             :             // This code is needed for geo'ish-parquet file that aren't GeoParquet 1.1,
     570             :             // nor use Parquet new native geometry/geography field columns, but
     571             :             // have a ARROW:extension:name == ogc.wkb field metadata,
     572             :             // and when libarrow extension handling geoarrow.wkb is NOT loaded
     573             :             // Such extension is automatically registered in OGRParquetDriverOpen()
     574             :             // for libarrow >= 21. It may also be registered by Python geoarrow.pyarrow
     575           9 :             osExtensionName = *extension_name;
     576             :         }
     577             :     }
     578             : 
     579       32716 :     std::shared_ptr<arrow::DataType> fieldType = field->type();
     580       32716 :     const auto fieldTypeId = fieldType->id();
     581       32716 :     if (osExtensionName.empty() && fieldTypeId == arrow::Type::EXTENSION)
     582             :     {
     583             :         // This code is needed for geo'ish-parquet file that aren't GeoParquet 1.1,
     584             :         // nor use Parquet new native geometry/geography field columns, but
     585             :         // have a ARROW:extension:name == ogc.wkb field metadata,
     586             :         // and when libarrow extension handling geoarrow.wkb is loaded
     587             :         // Such extension is automatically registered in OGRParquetDriverOpen()
     588             :         // for libarrow >= 21. It may also be registered by Python geoarrow.pyarrow
     589             :         auto extensionType =
     590          49 :             cpl::down_cast<arrow::ExtensionType *>(fieldType.get());
     591          49 :         osExtensionName = extensionType->extension_name();
     592             :     }
     593             : 
     594       32716 :     bool bRegularField = true;
     595             : 
     596       32716 :     auto oIter = m_oMapGeometryColumns.find(field->name());
     597             :     // cppcheck-suppress knownConditionTrueFalse
     598       64073 :     if (bRegularField && (oIter != m_oMapGeometryColumns.end() ||
     599       31357 :                           STARTS_WITH(osExtensionName.c_str(), "ogc.") ||
     600       31357 :                           STARTS_WITH(osExtensionName.c_str(), "geoarrow.")))
     601             :     {
     602        2722 :         CPLJSONObject oJSONDef;
     603        1361 :         if (oIter != m_oMapGeometryColumns.end())
     604        1359 :             oJSONDef = oIter->second;
     605        4083 :         auto osEncoding = oJSONDef.GetString("encoding");
     606        1361 :         if (osEncoding.empty() && !osExtensionName.empty())
     607           2 :             osEncoding = osExtensionName;
     608             : 
     609        1361 :         OGRwkbGeometryType eGeomType = wkbUnknown;
     610        1361 :         auto eGeomEncoding = OGRArrowGeomEncoding::WKB;
     611        1361 :         if (IsValidGeometryEncoding(field, osEncoding,
     612        2722 :                                     oIter != m_oMapGeometryColumns.end(),
     613             :                                     eGeomType, eGeomEncoding))
     614             :         {
     615        1361 :             bRegularField = false;
     616        2722 :             OGRGeomFieldDefn oField(field->name().c_str(), wkbUnknown);
     617             : 
     618        4083 :             auto oCRS = oJSONDef["crs"];
     619        1361 :             OGRSpatialReference *poSRS = nullptr;
     620        1361 :             if (!oCRS.IsValid())
     621             :             {
     622          50 :                 if (!m_oMapGeometryColumns.empty())
     623             :                 {
     624             :                     // WGS 84 is implied if no crs member is found.
     625          48 :                     poSRS = new OGRSpatialReference();
     626          48 :                     poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     627          48 :                     poSRS->importFromEPSG(4326);
     628             :                 }
     629             :             }
     630        1311 :             else if (oCRS.GetType() == CPLJSONObject::Type::String)
     631             :             {
     632        1119 :                 const auto osWKT = oCRS.ToString();
     633         373 :                 poSRS = new OGRSpatialReference();
     634         373 :                 poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     635             : 
     636         373 :                 if (poSRS->importFromWkt(osWKT.c_str()) != OGRERR_NONE)
     637             :                 {
     638           0 :                     poSRS->Release();
     639           0 :                     poSRS = nullptr;
     640             :                 }
     641             :             }
     642         938 :             else if (oCRS.GetType() == CPLJSONObject::Type::Object)
     643             :             {
     644             :                 // CRS encoded as PROJJSON (extension)
     645         120 :                 const auto oType = oCRS["type"];
     646          80 :                 if (oType.IsValid() &&
     647          40 :                     oType.GetType() == CPLJSONObject::Type::String)
     648             :                 {
     649         120 :                     const auto osType = oType.ToString();
     650          40 :                     if (osType.find("CRS") != std::string::npos)
     651             :                     {
     652          40 :                         poSRS = new OGRSpatialReference();
     653          40 :                         poSRS->SetAxisMappingStrategy(
     654             :                             OAMS_TRADITIONAL_GIS_ORDER);
     655             : 
     656          80 :                         if (poSRS->SetFromUserInput(
     657          80 :                                 oCRS.ToString().c_str(),
     658             :                                 OGRSpatialReference::
     659          40 :                                     SET_FROM_USER_INPUT_LIMITATIONS_get()) !=
     660             :                             OGRERR_NONE)
     661             :                         {
     662           0 :                             poSRS->Release();
     663           0 :                             poSRS = nullptr;
     664             :                         }
     665             :                     }
     666             :                 }
     667             :             }
     668             : 
     669        1361 :             if (poSRS)
     670             :             {
     671         461 :                 const double dfCoordEpoch = oJSONDef.GetDouble("epoch");
     672         461 :                 if (dfCoordEpoch > 0)
     673           4 :                     poSRS->SetCoordinateEpoch(dfCoordEpoch);
     674             : 
     675         461 :                 oField.SetSpatialRef(poSRS);
     676             : 
     677         461 :                 poSRS->Release();
     678             :             }
     679             : 
     680        1361 :             if (!m_osCRS.empty())
     681             :             {
     682           0 :                 poSRS = new OGRSpatialReference();
     683           0 :                 poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     684           0 :                 if (poSRS->SetFromUserInput(
     685             :                         m_osCRS.c_str(),
     686             :                         OGRSpatialReference::
     687           0 :                             SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
     688             :                     OGRERR_NONE)
     689             :                 {
     690           0 :                     oField.SetSpatialRef(poSRS);
     691             :                 }
     692           0 :                 poSRS->Release();
     693             :             }
     694             : 
     695        1361 :             if (oJSONDef.GetString("edges") == "spherical")
     696             :             {
     697           5 :                 SetMetadataItem("EDGES", "SPHERICAL");
     698             :             }
     699             : 
     700             :             // m_aeGeomEncoding be filled before calling
     701             :             // ComputeGeometryColumnType()
     702        1361 :             m_aeGeomEncoding.push_back(eGeomEncoding);
     703        1361 :             if (eGeomType == wkbUnknown)
     704             :             {
     705             :                 // geometry_types since 1.0.0-beta1. Was geometry_type
     706             :                 // before
     707        1812 :                 auto oType = oJSONDef.GetObj("geometry_types");
     708         604 :                 if (!oType.IsValid())
     709         377 :                     oType = oJSONDef.GetObj("geometry_type");
     710         604 :                 if (oType.GetType() == CPLJSONObject::Type::String)
     711             :                 {
     712             :                     // string is no longer valid since 1.0.0-beta1
     713           3 :                     const auto osType = oType.ToString();
     714           1 :                     if (osType != "Unknown")
     715           1 :                         eGeomType = GetGeometryTypeFromString(osType);
     716             :                 }
     717         603 :                 else if (oType.GetType() == CPLJSONObject::Type::Array)
     718             :                 {
     719         454 :                     const auto oTypeArray = oType.ToArray();
     720         227 :                     if (oTypeArray.Size() == 1)
     721             :                     {
     722         114 :                         eGeomType =
     723         114 :                             GetGeometryTypeFromString(oTypeArray[0].ToString());
     724             :                     }
     725         113 :                     else if (oTypeArray.Size() > 1)
     726             :                     {
     727             :                         const auto PromoteToCollection =
     728         266 :                             [](OGRwkbGeometryType eType)
     729             :                         {
     730         266 :                             if (eType == wkbPoint)
     731          41 :                                 return wkbMultiPoint;
     732         225 :                             if (eType == wkbLineString)
     733          36 :                                 return wkbMultiLineString;
     734         189 :                             if (eType == wkbPolygon)
     735          49 :                                 return wkbMultiPolygon;
     736         140 :                             return eType;
     737             :                         };
     738          50 :                         bool bMixed = false;
     739          50 :                         bool bHasMulti = false;
     740          50 :                         bool bHasZ = false;
     741          50 :                         bool bHasM = false;
     742             :                         const auto eFirstType =
     743          50 :                             OGR_GT_Flatten(GetGeometryTypeFromString(
     744         100 :                                 oTypeArray[0].ToString()));
     745             :                         const auto eFirstTypeCollection =
     746          50 :                             PromoteToCollection(eFirstType);
     747         142 :                         for (int i = 0; i < oTypeArray.Size(); ++i)
     748             :                         {
     749         124 :                             const auto eThisGeom = GetGeometryTypeFromString(
     750         248 :                                 oTypeArray[i].ToString());
     751         124 :                             if (PromoteToCollection(OGR_GT_Flatten(
     752         124 :                                     eThisGeom)) != eFirstTypeCollection)
     753             :                             {
     754          32 :                                 bMixed = true;
     755          32 :                                 break;
     756             :                             }
     757          92 :                             bHasZ |= OGR_GT_HasZ(eThisGeom) != FALSE;
     758          92 :                             bHasM |= OGR_GT_HasM(eThisGeom) != FALSE;
     759          92 :                             bHasMulti |=
     760          92 :                                 (PromoteToCollection(OGR_GT_Flatten(
     761          92 :                                      eThisGeom)) == OGR_GT_Flatten(eThisGeom));
     762             :                         }
     763          50 :                         if (!bMixed)
     764             :                         {
     765          18 :                             if (eFirstTypeCollection == wkbMultiPolygon ||
     766             :                                 eFirstTypeCollection == wkbMultiLineString)
     767             :                             {
     768          17 :                                 if (bHasMulti)
     769          17 :                                     eGeomType = OGR_GT_SetModifier(
     770             :                                         eFirstTypeCollection, bHasZ, bHasM);
     771             :                                 else
     772           0 :                                     eGeomType = OGR_GT_SetModifier(
     773             :                                         eFirstType, bHasZ, bHasM);
     774             :                             }
     775             :                         }
     776             :                     }
     777             :                 }
     778         376 :                 else if (CPLTestBool(CPLGetConfigOption(
     779             :                              "OGR_PARQUET_COMPUTE_GEOMETRY_TYPE", "YES")))
     780             :                 {
     781         376 :                     eGeomType = computeGeometryTypeFun();
     782             :                 }
     783             :             }
     784             : 
     785        1361 :             oField.SetType(eGeomType);
     786        1361 :             oField.SetNullable(field->nullable());
     787        1361 :             m_poFeatureDefn->AddGeomFieldDefn(&oField);
     788        1361 :             m_anMapGeomFieldIndexToArrowColumn.push_back(iFieldIdx);
     789             :         }
     790             :     }
     791             : 
     792             :     // Try to autodetect a (WKB) geometry column from the GEOM_POSSIBLE_NAMES
     793             :     // open option
     794       62655 :     if (bRegularField && osExtensionName.empty() &&
     795       95371 :         m_oMapGeometryColumns.empty() &&
     796         275 :         m_aosGeomPossibleNames.FindString(field->name().c_str()) >= 0)
     797             :     {
     798          13 :         if (fieldTypeId == arrow::Type::BINARY ||
     799             :             fieldTypeId == arrow::Type::LARGE_BINARY)
     800             :         {
     801           7 :             CPLDebug("PARQUET",
     802             :                      "Field %s detected as likely WKB geometry field",
     803           7 :                      field->name().c_str());
     804           7 :             bRegularField = false;
     805           7 :             m_aeGeomEncoding.push_back(OGRArrowGeomEncoding::WKB);
     806             :         }
     807           0 :         else if ((fieldTypeId == arrow::Type::STRING ||
     808          16 :                   fieldTypeId == arrow::Type::LARGE_STRING) &&
     809          10 :                  (field->name().find("wkt") != std::string::npos ||
     810           4 :                   field->name().find("WKT") != std::string::npos))
     811             :         {
     812           2 :             CPLDebug("PARQUET",
     813             :                      "Field %s detected as likely WKT geometry field",
     814           2 :                      field->name().c_str());
     815           2 :             bRegularField = false;
     816           2 :             m_aeGeomEncoding.push_back(OGRArrowGeomEncoding::WKT);
     817             :         }
     818          13 :         if (!bRegularField)
     819             :         {
     820          18 :             OGRGeomFieldDefn oField(field->name().c_str(), wkbUnknown);
     821           9 :             oField.SetNullable(field->nullable());
     822             : 
     823           9 :             if (!m_osCRS.empty())
     824             :             {
     825           2 :                 auto poSRS = new OGRSpatialReference();
     826           2 :                 poSRS->SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
     827           2 :                 if (poSRS->SetFromUserInput(
     828             :                         m_osCRS.c_str(),
     829             :                         OGRSpatialReference::
     830           2 :                             SET_FROM_USER_INPUT_LIMITATIONS_get()) ==
     831             :                     OGRERR_NONE)
     832             :                 {
     833           2 :                     oField.SetSpatialRef(poSRS);
     834             :                 }
     835           2 :                 poSRS->Release();
     836             :             }
     837             : 
     838           9 :             m_poFeatureDefn->AddGeomFieldDefn(&oField);
     839           9 :             m_anMapGeomFieldIndexToArrowColumn.push_back(iFieldIdx);
     840             :         }
     841             :     }
     842             : 
     843       65432 :     return !bRegularField;
     844             : }
     845             : 
     846             : /************************************************************************/
     847             : /*                           TestCapability()                           */
     848             : /************************************************************************/
     849             : 
     850         768 : int OGRParquetLayerBase::TestCapability(const char *pszCap) const
     851             : {
     852         768 :     if (EQUAL(pszCap, OLCMeasuredGeometries))
     853          32 :         return true;
     854             : 
     855         736 :     if (EQUAL(pszCap, OLCFastSetNextByIndex))
     856           0 :         return true;
     857             : 
     858         736 :     if (EQUAL(pszCap, OLCFastSpatialFilter))
     859             :     {
     860          73 :         if (m_oMapGeomFieldIndexToGeomColBBOX.find(m_iGeomFieldFilter) !=
     861         146 :             m_oMapGeomFieldIndexToGeomColBBOX.end())
     862             :         {
     863          49 :             return true;
     864             :         }
     865          24 :         return false;
     866             :     }
     867             : 
     868         663 :     return OGRArrowLayer::TestCapability(pszCap);
     869             : }
     870             : 
     871             : /************************************************************************/
     872             : /*                             GetNumCPUs()                             */
     873             : /************************************************************************/
     874             : 
     875             : /* static */
     876        2000 : int OGRParquetLayerBase::GetNumCPUs()
     877             : {
     878        2000 :     const char *pszNumThreads = nullptr;
     879             :     int nNumThreads =
     880        2000 :         GDALGetNumThreads(pszNumThreads,
     881             :                           /* nMaxVal = */ -1,
     882             :                           /* bDefaultToAllCPUs = */ false, &pszNumThreads);
     883        2000 :     if (pszNumThreads == nullptr)
     884           0 :         nNumThreads = std::min(4, CPLGetNumCPUs());
     885        2000 :     if (nNumThreads > 1)
     886             :     {
     887           0 :         CPL_IGNORE_RET_VAL(arrow::SetCpuThreadPoolCapacity(nNumThreads));
     888             :     }
     889        2000 :     return nNumThreads;
     890             : }
     891             : 
     892             : /************************************************************************/
     893             : /*                          OGRParquetLayer()                           */
     894             : /************************************************************************/
     895             : 
     896        1058 : OGRParquetLayer::OGRParquetLayer(
     897             :     OGRParquetDataset *poDS, const char *pszLayerName,
     898             :     std::unique_ptr<parquet::arrow::FileReader> &&arrow_reader,
     899        1058 :     CSLConstList papszOpenOptions)
     900             :     : OGRParquetLayerBase(poDS, pszLayerName, papszOpenOptions),
     901        1058 :       m_poArrowReader(std::move(arrow_reader))
     902             : {
     903        1058 :     EstablishFeatureDefn();
     904        1058 :     CPLAssert(static_cast<int>(m_aeGeomEncoding.size()) ==
     905             :               m_poFeatureDefn->GetGeomFieldCount());
     906             : 
     907        1058 :     m_oFeatureIdxRemappingIter = m_asFeatureIdxRemapping.begin();
     908        1058 : }
     909             : 
     910             : /************************************************************************/
     911             : /*                        EstablishFeatureDefn()                        */
     912             : /************************************************************************/
     913             : 
     914        1058 : void OGRParquetLayer::EstablishFeatureDefn()
     915             : {
     916        1058 :     const auto metadata = m_poArrowReader->parquet_reader()->metadata();
     917        1058 :     const auto &kv_metadata = metadata->key_value_metadata();
     918             : 
     919        1058 :     LoadGeoMetadata(kv_metadata);
     920             :     const auto oMapFieldNameToGDALSchemaFieldDefn =
     921        1058 :         LoadGDALSchema(kv_metadata.get());
     922             : 
     923        1058 :     LoadGDALMetadata(kv_metadata.get());
     924             : 
     925        1058 :     if (kv_metadata && kv_metadata->Contains("gdal:creation-options"))
     926             :     {
     927        1110 :         auto co = kv_metadata->Get("gdal:creation-options");
     928         555 :         if (co.ok())
     929             :         {
     930         555 :             CPLDebugOnly("PARQUET", "gdal:creation-options = %s", co->c_str());
     931        1110 :             CPLJSONDocument oDoc;
     932         555 :             if (oDoc.LoadMemory(*co))
     933             :             {
     934        1110 :                 auto oRoot = oDoc.GetRoot();
     935         555 :                 if (oRoot.GetType() == CPLJSONObject::Type::Object)
     936             :                 {
     937        1786 :                     for (const auto &oChild : oRoot.GetChildren())
     938             :                     {
     939        1231 :                         if (oChild.GetType() == CPLJSONObject::Type::String)
     940             :                         {
     941             :                             m_aosCreationOptions.SetNameValue(
     942        2462 :                                 oChild.GetName().c_str(),
     943        3693 :                                 oChild.ToString().c_str());
     944             :                         }
     945             :                     }
     946             :                 }
     947             :             }
     948             :         }
     949             :     }
     950             : 
     951        1058 :     if (!m_poArrowReader->GetSchema(&m_poSchema).ok())
     952             :     {
     953           0 :         return;
     954             :     }
     955             : 
     956             :     const bool bUseBBOX =
     957        1058 :         CPLTestBool(CPLGetConfigOption("OGR_PARQUET_USE_BBOX", "YES"));
     958             : 
     959             :     // Keep track of declared bounding box columns in GeoParquet JSON metadata,
     960             :     // in order not to expose them as regular fields.
     961        2116 :     std::set<std::string> oSetBBOXColumns;
     962        1058 :     if (bUseBBOX)
     963             :     {
     964        2070 :         for (const auto &iter : m_oMapGeometryColumns)
     965             :         {
     966        2034 :             std::string osBBOXColumn;
     967        2034 :             std::string osXMin, osYMin, osXMax, osYMax;
     968        1017 :             if (ParseGeometryColumnCovering(iter.second, osBBOXColumn, osXMin,
     969             :                                             osYMin, osXMax, osYMax))
     970             :             {
     971         520 :                 oSetBBOXColumns.insert(std::move(osBBOXColumn));
     972             :             }
     973             :         }
     974             :     }
     975             : 
     976        1058 :     const auto &fields = m_poSchema->fields();
     977        1058 :     const auto poParquetSchema = metadata->schema();
     978             : 
     979             :     // Map from Parquet column name (with dot separator) to Parquet index
     980        2116 :     std::map<std::string, int> oMapParquetColumnNameToIdx;
     981        1058 :     const int nParquetColumns = poParquetSchema->num_columns();
     982       37317 :     for (int iParquetCol = 0; iParquetCol < nParquetColumns; ++iParquetCol)
     983             :     {
     984       36259 :         const auto parquetColumn = poParquetSchema->Column(iParquetCol);
     985       36259 :         const auto parquetColumnName = parquetColumn->path()->ToDotString();
     986       36259 :         oMapParquetColumnNameToIdx[parquetColumnName] = iParquetCol;
     987             :     }
     988             : 
     989             :     // Synthetize a GeoParquet bounding box column definition when detecting
     990             :     // a Overture Map dataset < 2024-04-16-beta.0
     991        1004 :     if ((m_oMapGeometryColumns.empty() ||
     992             :          // Below is for release 2024-01-17-alpha.0
     993        2062 :          (m_oMapGeometryColumns.find("geometry") !=
     994        2062 :               m_oMapGeometryColumns.end() &&
     995        2546 :           !m_oMapGeometryColumns["geometry"].GetObj("covering").IsValid() &&
     996        1922 :           m_oMapGeometryColumns["geometry"].GetString("encoding") == "WKB")) &&
     997         365 :         bUseBBOX &&
     998        1423 :         oMapParquetColumnNameToIdx.find("geometry") !=
     999        1743 :             oMapParquetColumnNameToIdx.end() &&
    1000        1378 :         oMapParquetColumnNameToIdx.find("bbox.minx") !=
    1001        1379 :             oMapParquetColumnNameToIdx.end() &&
    1002        1059 :         oMapParquetColumnNameToIdx.find("bbox.miny") !=
    1003        1060 :             oMapParquetColumnNameToIdx.end() &&
    1004        1059 :         oMapParquetColumnNameToIdx.find("bbox.maxx") !=
    1005        3175 :             oMapParquetColumnNameToIdx.end() &&
    1006        1059 :         oMapParquetColumnNameToIdx.find("bbox.maxy") !=
    1007        1059 :             oMapParquetColumnNameToIdx.end())
    1008             :     {
    1009           2 :         CPLJSONObject oDef;
    1010           1 :         if (m_oMapGeometryColumns.find("geometry") !=
    1011           2 :             m_oMapGeometryColumns.end())
    1012             :         {
    1013           0 :             oDef = m_oMapGeometryColumns["geometry"];
    1014             :         }
    1015           2 :         CPLJSONObject oCovering;
    1016           1 :         oDef.Add("covering", oCovering);
    1017           1 :         CPLJSONObject oBBOX;
    1018           1 :         oCovering.Add("bbox", oBBOX);
    1019             :         {
    1020           1 :             CPLJSONArray oArray;
    1021           1 :             oArray.Add("bbox");
    1022           1 :             oArray.Add("minx");
    1023           1 :             oBBOX.Add("xmin", oArray);
    1024             :         }
    1025             :         {
    1026           1 :             CPLJSONArray oArray;
    1027           1 :             oArray.Add("bbox");
    1028           1 :             oArray.Add("miny");
    1029           1 :             oBBOX.Add("ymin", oArray);
    1030             :         }
    1031             :         {
    1032           1 :             CPLJSONArray oArray;
    1033           1 :             oArray.Add("bbox");
    1034           1 :             oArray.Add("maxx");
    1035           1 :             oBBOX.Add("xmax", oArray);
    1036             :         }
    1037             :         {
    1038           1 :             CPLJSONArray oArray;
    1039           1 :             oArray.Add("bbox");
    1040           1 :             oArray.Add("maxy");
    1041           1 :             oBBOX.Add("ymax", oArray);
    1042             :         }
    1043           1 :         oSetBBOXColumns.insert("bbox");
    1044           1 :         oDef.Add("encoding", "WKB");
    1045           1 :         m_oMapGeometryColumns["geometry"] = std::move(oDef);
    1046             :     }
    1047             :     // Overture Maps 2024-04-16-beta.0 almost follows GeoParquet 1.1, except
    1048             :     // they don't declare the "covering" element in the GeoParquet JSON metadata
    1049        2114 :     else if (m_oMapGeometryColumns.find("geometry") !=
    1050        2048 :                  m_oMapGeometryColumns.end() &&
    1051        1974 :              bUseBBOX &&
    1052        2540 :              !m_oMapGeometryColumns["geometry"].GetObj("covering").IsValid() &&
    1053        1865 :              m_oMapGeometryColumns["geometry"].GetString("encoding") == "WKB" &&
    1054        1369 :              oMapParquetColumnNameToIdx.find("geometry") !=
    1055        1681 :                  oMapParquetColumnNameToIdx.end() &&
    1056        1369 :              oMapParquetColumnNameToIdx.find("bbox.xmin") !=
    1057        1371 :                  oMapParquetColumnNameToIdx.end() &&
    1058        1059 :              oMapParquetColumnNameToIdx.find("bbox.ymin") !=
    1059        1061 :                  oMapParquetColumnNameToIdx.end() &&
    1060        1059 :              oMapParquetColumnNameToIdx.find("bbox.xmax") !=
    1061        4164 :                  oMapParquetColumnNameToIdx.end() &&
    1062        1059 :              oMapParquetColumnNameToIdx.find("bbox.ymax") !=
    1063        1059 :                  oMapParquetColumnNameToIdx.end())
    1064             :     {
    1065           6 :         CPLJSONObject oDef = m_oMapGeometryColumns["geometry"];
    1066           4 :         CPLJSONObject oCovering;
    1067           2 :         oDef.Add("covering", oCovering);
    1068           2 :         CPLJSONObject oBBOX;
    1069           2 :         oCovering.Add("bbox", oBBOX);
    1070             :         {
    1071           2 :             CPLJSONArray oArray;
    1072           2 :             oArray.Add("bbox");
    1073           2 :             oArray.Add("xmin");
    1074           2 :             oBBOX.Add("xmin", oArray);
    1075             :         }
    1076             :         {
    1077           2 :             CPLJSONArray oArray;
    1078           2 :             oArray.Add("bbox");
    1079           2 :             oArray.Add("ymin");
    1080           2 :             oBBOX.Add("ymin", oArray);
    1081             :         }
    1082             :         {
    1083           2 :             CPLJSONArray oArray;
    1084           2 :             oArray.Add("bbox");
    1085           2 :             oArray.Add("xmax");
    1086           2 :             oBBOX.Add("xmax", oArray);
    1087             :         }
    1088             :         {
    1089           2 :             CPLJSONArray oArray;
    1090           2 :             oArray.Add("bbox");
    1091           2 :             oArray.Add("ymax");
    1092           2 :             oBBOX.Add("ymax", oArray);
    1093             :         }
    1094           2 :         oSetBBOXColumns.insert("bbox");
    1095           2 :         m_oMapGeometryColumns["geometry"] = std::move(oDef);
    1096             :     }
    1097             : 
    1098        1058 :     int iParquetCol = 0;
    1099       27344 :     for (int i = 0; i < m_poSchema->num_fields(); ++i)
    1100             :     {
    1101       26286 :         const auto &field = fields[i];
    1102             : 
    1103             :         bool bParquetColValid =
    1104       26286 :             CheckMatchArrowParquetColumnNames(iParquetCol, field);
    1105       26286 :         if (!bParquetColValid)
    1106           0 :             m_bHasMissingMappingToParquet = true;
    1107             : 
    1108       26330 :         if (!m_osFIDColumn.empty() && field->name() == m_osFIDColumn &&
    1109          44 :             (field->type()->id() == arrow::Type::INT32 ||
    1110          22 :              field->type()->id() == arrow::Type::INT64))
    1111             :         {
    1112          22 :             m_poFIDType = field->type();
    1113          22 :             m_iFIDArrowColumn = i;
    1114          22 :             if (bParquetColValid)
    1115             :             {
    1116          22 :                 m_iFIDParquetColumn = iParquetCol;
    1117          22 :                 iParquetCol++;
    1118             :             }
    1119         545 :             continue;
    1120             :         }
    1121             : 
    1122       26264 :         if (oSetBBOXColumns.find(field->name()) != oSetBBOXColumns.end())
    1123             :         {
    1124         523 :             m_oSetBBoxArrowColumns.insert(i);
    1125         523 :             if (bParquetColValid)
    1126         523 :                 iParquetCol++;
    1127         523 :             continue;
    1128             :         }
    1129             : 
    1130             :         const auto ComputeGeometryColumnTypeLambda =
    1131         891 :             [this, bParquetColValid, iParquetCol, &poParquetSchema]()
    1132             :         {
    1133             :             // only with GeoParquet < 0.2.0
    1134         594 :             if (bParquetColValid &&
    1135         297 :                 poParquetSchema->Column(iParquetCol)->physical_type() ==
    1136             :                     parquet::Type::BYTE_ARRAY)
    1137             :             {
    1138         297 :                 return ComputeGeometryColumnType(
    1139         594 :                     m_poFeatureDefn->GetGeomFieldCount(), iParquetCol);
    1140             :             }
    1141           0 :             return wkbUnknown;
    1142       25741 :         };
    1143             : 
    1144       77223 :         const bool bGeometryField = DealWithGeometryColumn(
    1145             :             i, field, ComputeGeometryColumnTypeLambda,
    1146       25741 :             bParquetColValid ? poParquetSchema->Column(iParquetCol) : nullptr,
    1147       25741 :             metadata.get(), bParquetColValid ? iParquetCol : -1);
    1148       25741 :         if (bGeometryField)
    1149             :         {
    1150        1031 :             const auto oIter = m_oMapGeometryColumns.find(field->name());
    1151        1031 :             if (bUseBBOX && oIter != m_oMapGeometryColumns.end())
    1152             :             {
    1153        1017 :                 ProcessGeometryColumnCovering(field, oIter->second,
    1154             :                                               oMapParquetColumnNameToIdx);
    1155             :             }
    1156             : 
    1157        3033 :             if (bParquetColValid &&
    1158        2002 :                 (field->type()->id() == arrow::Type::STRUCT ||
    1159         971 :                  field->type()->id() == arrow::Type::LIST))
    1160             :             {
    1161             :                 // GeoArrow types
    1162         493 :                 std::vector<int> anParquetCols;
    1163        3346 :                 for (const auto &iterParquetCols : oMapParquetColumnNameToIdx)
    1164             :                 {
    1165        2853 :                     if (STARTS_WITH(
    1166             :                             iterParquetCols.first.c_str(),
    1167             :                             std::string(field->name()).append(".").c_str()))
    1168             :                     {
    1169        1094 :                         iParquetCol =
    1170        1094 :                             std::max(iParquetCol, iterParquetCols.second);
    1171        1094 :                         anParquetCols.push_back(iterParquetCols.second);
    1172             :                     }
    1173             :                 }
    1174         493 :                 m_anMapGeomFieldIndexToParquetColumns.push_back(
    1175         493 :                     std::move(anParquetCols));
    1176         493 :                 ++iParquetCol;
    1177             :             }
    1178             :             else
    1179             :             {
    1180         538 :                 m_anMapGeomFieldIndexToParquetColumns.push_back(
    1181         538 :                     {bParquetColValid ? iParquetCol : -1});
    1182         538 :                 if (bParquetColValid)
    1183         538 :                     iParquetCol++;
    1184             :             }
    1185             :         }
    1186             :         else
    1187             :         {
    1188       24710 :             CreateFieldFromSchema(field, bParquetColValid, iParquetCol, {i},
    1189             :                                   oMapFieldNameToGDALSchemaFieldDefn);
    1190             :         }
    1191             :     }
    1192             : 
    1193        1058 :     CPLAssert(static_cast<int>(m_anMapFieldIndexToArrowColumn.size()) ==
    1194             :               m_poFeatureDefn->GetFieldCount());
    1195        1058 :     CPLAssert(static_cast<int>(m_anMapGeomFieldIndexToArrowColumn.size()) ==
    1196             :               m_poFeatureDefn->GetGeomFieldCount());
    1197        1058 :     CPLAssert(static_cast<int>(m_anMapGeomFieldIndexToParquetColumns.size()) ==
    1198             :               m_poFeatureDefn->GetGeomFieldCount());
    1199             : 
    1200        1058 :     if (!fields.empty())
    1201             :     {
    1202             :         try
    1203             :         {
    1204        2061 :             auto poRowGroup = m_poArrowReader->parquet_reader()->RowGroup(0);
    1205        1004 :             if (poRowGroup)
    1206             :             {
    1207        2008 :                 auto poColumn = poRowGroup->metadata()->ColumnChunk(0);
    1208        1004 :                 CPLDebug("PARQUET", "Compression (of first column): %s",
    1209             :                          arrow::util::Codec::GetCodecAsString(
    1210        1004 :                              poColumn->compression())
    1211             :                              .c_str());
    1212             :             }
    1213             :         }
    1214          53 :         catch (const std::exception &)
    1215             :         {
    1216             :         }
    1217             :     }
    1218             : }
    1219             : 
    1220             : /************************************************************************/
    1221             : /*                   ProcessGeometryColumnCovering()                    */
    1222             : /************************************************************************/
    1223             : 
    1224             : /** Process GeoParquet JSON geometry field object to extract information about
    1225             :  * its bounding box column, and appropriately fill m_oMapGeomFieldIndexToGeomColBBOX
    1226             :  * and m_oMapGeomFieldIndexToGeomColBBOXParquet members with information on that
    1227             :  * bounding box column.
    1228             :  */
    1229        1017 : void OGRParquetLayer::ProcessGeometryColumnCovering(
    1230             :     const std::shared_ptr<arrow::Field> &field,
    1231             :     const CPLJSONObject &oJSONGeometryColumn,
    1232             :     const std::map<std::string, int> &oMapParquetColumnNameToIdx)
    1233             : {
    1234        2034 :     std::string osBBOXColumn;
    1235        2034 :     std::string osXMin, osYMin, osXMax, osYMax;
    1236        1017 :     if (ParseGeometryColumnCovering(oJSONGeometryColumn, osBBOXColumn, osXMin,
    1237             :                                     osYMin, osXMax, osYMax))
    1238             :     {
    1239         523 :         OGRArrowLayer::GeomColBBOX sDesc;
    1240         523 :         sDesc.iArrowCol = m_poSchema->GetFieldIndex(osBBOXColumn);
    1241        1046 :         const auto fieldBBOX = m_poSchema->GetFieldByName(osBBOXColumn);
    1242        1046 :         if (sDesc.iArrowCol >= 0 && fieldBBOX &&
    1243         523 :             fieldBBOX->type()->id() == arrow::Type::STRUCT)
    1244             :         {
    1245             :             const auto fieldBBOXStruct =
    1246        1046 :                 std::static_pointer_cast<arrow::StructType>(fieldBBOX->type());
    1247        1046 :             const auto fieldXMin = fieldBBOXStruct->GetFieldByName(osXMin);
    1248        1046 :             const auto fieldYMin = fieldBBOXStruct->GetFieldByName(osYMin);
    1249        1046 :             const auto fieldXMax = fieldBBOXStruct->GetFieldByName(osXMax);
    1250        1046 :             const auto fieldYMax = fieldBBOXStruct->GetFieldByName(osYMax);
    1251         523 :             const int nXMinIdx = fieldBBOXStruct->GetFieldIndex(osXMin);
    1252         523 :             const int nYMinIdx = fieldBBOXStruct->GetFieldIndex(osYMin);
    1253         523 :             const int nXMaxIdx = fieldBBOXStruct->GetFieldIndex(osXMax);
    1254         523 :             const int nYMaxIdx = fieldBBOXStruct->GetFieldIndex(osYMax);
    1255             :             const auto oIterParquetIdxXMin = oMapParquetColumnNameToIdx.find(
    1256         523 :                 std::string(osBBOXColumn).append(".").append(osXMin));
    1257             :             const auto oIterParquetIdxYMin = oMapParquetColumnNameToIdx.find(
    1258         523 :                 std::string(osBBOXColumn).append(".").append(osYMin));
    1259             :             const auto oIterParquetIdxXMax = oMapParquetColumnNameToIdx.find(
    1260         523 :                 std::string(osBBOXColumn).append(".").append(osXMax));
    1261             :             const auto oIterParquetIdxYMax = oMapParquetColumnNameToIdx.find(
    1262         523 :                 std::string(osBBOXColumn).append(".").append(osYMax));
    1263         523 :             if (nXMinIdx >= 0 && nYMinIdx >= 0 && nXMaxIdx >= 0 &&
    1264        1046 :                 nYMaxIdx >= 0 && fieldXMin && fieldYMin && fieldXMax &&
    1265        1046 :                 fieldYMax &&
    1266        1046 :                 oIterParquetIdxXMin != oMapParquetColumnNameToIdx.end() &&
    1267        1046 :                 oIterParquetIdxYMin != oMapParquetColumnNameToIdx.end() &&
    1268        1046 :                 oIterParquetIdxXMax != oMapParquetColumnNameToIdx.end() &&
    1269        1046 :                 oIterParquetIdxYMax != oMapParquetColumnNameToIdx.end() &&
    1270         525 :                 (fieldXMin->type()->id() == arrow::Type::FLOAT ||
    1271           2 :                  fieldXMin->type()->id() == arrow::Type::DOUBLE) &&
    1272         523 :                 fieldXMin->type()->id() == fieldYMin->type()->id() &&
    1273        1569 :                 fieldXMin->type()->id() == fieldXMax->type()->id() &&
    1274         523 :                 fieldXMin->type()->id() == fieldYMax->type()->id())
    1275             :             {
    1276         523 :                 CPLDebug("PARQUET",
    1277             :                          "Bounding box column '%s' detected for "
    1278             :                          "geometry column '%s'",
    1279         523 :                          osBBOXColumn.c_str(), field->name().c_str());
    1280         523 :                 sDesc.iArrowSubfieldXMin = nXMinIdx;
    1281         523 :                 sDesc.iArrowSubfieldYMin = nYMinIdx;
    1282         523 :                 sDesc.iArrowSubfieldXMax = nXMaxIdx;
    1283         523 :                 sDesc.iArrowSubfieldYMax = nYMaxIdx;
    1284         523 :                 sDesc.bIsFloat =
    1285         523 :                     (fieldXMin->type()->id() == arrow::Type::FLOAT);
    1286             : 
    1287             :                 m_oMapGeomFieldIndexToGeomColBBOX
    1288         523 :                     [m_poFeatureDefn->GetGeomFieldCount() - 1] =
    1289         523 :                         std::move(sDesc);
    1290             : 
    1291         523 :                 GeomColBBOXParquet sDescParquet;
    1292         523 :                 sDescParquet.iParquetXMin = oIterParquetIdxXMin->second;
    1293         523 :                 sDescParquet.iParquetYMin = oIterParquetIdxYMin->second;
    1294         523 :                 sDescParquet.iParquetXMax = oIterParquetIdxXMax->second;
    1295         523 :                 sDescParquet.iParquetYMax = oIterParquetIdxYMax->second;
    1296        5579 :                 for (const auto &iterParquetCols : oMapParquetColumnNameToIdx)
    1297             :                 {
    1298        5056 :                     if (STARTS_WITH(
    1299             :                             iterParquetCols.first.c_str(),
    1300             :                             std::string(osBBOXColumn).append(".").c_str()))
    1301             :                     {
    1302        2092 :                         sDescParquet.anParquetCols.push_back(
    1303        2092 :                             iterParquetCols.second);
    1304             :                     }
    1305             :                 }
    1306             :                 m_oMapGeomFieldIndexToGeomColBBOXParquet
    1307        1046 :                     [m_poFeatureDefn->GetGeomFieldCount() - 1] =
    1308        1046 :                         std::move(sDescParquet);
    1309             :             }
    1310             :         }
    1311             :     }
    1312        1017 : }
    1313             : 
    1314             : /************************************************************************/
    1315             : /*                              FindNode()                              */
    1316             : /************************************************************************/
    1317             : 
    1318      291898 : static const parquet::schema::Node *FindNode(const parquet::schema::Node *node,
    1319             :                                              const std::string &arrowFieldName)
    1320             : {
    1321      291898 :     CPLAssert(node);
    1322      291898 :     if (node->name() == arrowFieldName)
    1323             :     {
    1324        4837 :         return node;
    1325             :     }
    1326      287061 :     else if (node->is_group())
    1327             :     {
    1328             :         const auto groupNode =
    1329      113420 :             cpl::down_cast<const parquet::schema::GroupNode *>(node);
    1330      395642 :         for (int i = 0; i < groupNode->field_count(); ++i)
    1331             :         {
    1332             :             const auto found =
    1333      287059 :                 FindNode(groupNode->field(i).get(), arrowFieldName);
    1334      287059 :             if (found)
    1335        4837 :                 return found;
    1336             :         }
    1337             :     }
    1338      282224 :     return nullptr;
    1339             : }
    1340             : 
    1341             : /************************************************************************/
    1342             : /*                         CollectLeaveNodes()                          */
    1343             : /************************************************************************/
    1344             : 
    1345       12811 : static void CollectLeaveNodes(
    1346             :     const parquet::schema::Node *node,
    1347             :     const std::map<const parquet::schema::Node *, int> &oMapNodeToColIdx,
    1348             :     std::vector<int> &anParquetCols)
    1349             : {
    1350       12811 :     CPLAssert(node);
    1351       12811 :     if (node->is_primitive())
    1352             :     {
    1353        6976 :         const auto it = oMapNodeToColIdx.find(node);
    1354        6976 :         if (it != oMapNodeToColIdx.end())
    1355        6976 :             anParquetCols.push_back(it->second);
    1356             :     }
    1357        5835 :     else if (node->is_group())
    1358             :     {
    1359             :         const auto groupNode =
    1360        5835 :             cpl::down_cast<const parquet::schema::GroupNode *>(node);
    1361       13809 :         for (int i = 0; i < groupNode->field_count(); ++i)
    1362             :         {
    1363        7974 :             CollectLeaveNodes(groupNode->field(i).get(), oMapNodeToColIdx,
    1364             :                               anParquetCols);
    1365             :         }
    1366             :     }
    1367       12811 : }
    1368             : 
    1369             : /************************************************************************/
    1370             : /*                GetParquetColumnIndicesForArrowField()                */
    1371             : /************************************************************************/
    1372             : 
    1373        4839 : std::vector<int> OGRParquetLayer::GetParquetColumnIndicesForArrowField(
    1374             :     const std::string &arrowFieldName) const
    1375             : {
    1376        9678 :     const auto metadata = m_poArrowReader->parquet_reader()->metadata();
    1377        4839 :     const auto schema = metadata->schema();
    1378             : 
    1379        4839 :     std::vector<int> anParquetCols;
    1380        4839 :     const auto *rootNode = schema->schema_root().get();
    1381        4839 :     const auto *fieldNode = FindNode(rootNode, arrowFieldName);
    1382        4839 :     if (!fieldNode)
    1383             :     {
    1384           2 :         CPLDebug("Parquet",
    1385             :                  "Cannot find Parquet node corresponding to Arrow field %s",
    1386             :                  arrowFieldName.c_str());
    1387           2 :         return anParquetCols;
    1388             :     }
    1389             : 
    1390             :     /// Build mapping from schema node to column index
    1391        9674 :     std::map<const parquet::schema::Node *, int> oMapNodeToColIdx;
    1392        4837 :     const int num_cols = schema->num_columns();
    1393      505128 :     for (int i = 0; i < num_cols; ++i)
    1394             :     {
    1395      500291 :         const auto *node = schema->Column(i)->schema_node().get();
    1396      500291 :         oMapNodeToColIdx[node] = i;
    1397             :     }
    1398             : 
    1399        4837 :     CollectLeaveNodes(fieldNode, oMapNodeToColIdx, anParquetCols);
    1400             : 
    1401        4837 :     return anParquetCols;
    1402             : }
    1403             : 
    1404             : /************************************************************************/
    1405             : /*                 CheckMatchArrowParquetColumnNames()                  */
    1406             : /************************************************************************/
    1407             : 
    1408       28638 : bool OGRParquetLayer::CheckMatchArrowParquetColumnNames(
    1409             :     int &iParquetCol, const std::shared_ptr<arrow::Field> &field) const
    1410             : {
    1411       57276 :     const auto metadata = m_poArrowReader->parquet_reader()->metadata();
    1412       28638 :     const auto poParquetSchema = metadata->schema();
    1413       28638 :     const int nParquetColumns = poParquetSchema->num_columns();
    1414       28638 :     const auto &fieldName = field->name();
    1415       28638 :     const int iParquetColBefore = iParquetCol;
    1416             : 
    1417       29462 :     while (iParquetCol < nParquetColumns)
    1418             :     {
    1419       29462 :         const auto parquetColumn = poParquetSchema->Column(iParquetCol);
    1420       29462 :         const auto parquetColumnName = parquetColumn->path()->ToDotString();
    1421       62840 :         if (fieldName == parquetColumnName ||
    1422       16689 :             (parquetColumnName.size() > fieldName.size() &&
    1423       16689 :              STARTS_WITH(parquetColumnName.c_str(), fieldName.c_str()) &&
    1424       15865 :              parquetColumnName[fieldName.size()] == '.'))
    1425             :         {
    1426       28638 :             return true;
    1427             :         }
    1428             :         else
    1429             :         {
    1430         824 :             iParquetCol++;
    1431             :         }
    1432             :     }
    1433             : 
    1434           0 :     CPLError(CE_Warning, CPLE_AppDefined,
    1435             :              "Cannot match Arrow column name %s with a Parquet one",
    1436             :              fieldName.c_str());
    1437           0 :     iParquetCol = iParquetColBefore;
    1438           0 :     return false;
    1439             : }
    1440             : 
    1441             : /************************************************************************/
    1442             : /*                       CreateFieldFromSchema()                        */
    1443             : /************************************************************************/
    1444             : 
    1445       27062 : void OGRParquetLayer::CreateFieldFromSchema(
    1446             :     const std::shared_ptr<arrow::Field> &field, bool bParquetColValid,
    1447             :     int &iParquetCol, const std::vector<int> &path,
    1448             :     const std::map<std::string, std::unique_ptr<OGRFieldDefn>>
    1449             :         &oMapFieldNameToGDALSchemaFieldDefn)
    1450             : {
    1451       27062 :     OGRFieldDefn oField(field->name().c_str(), OFTString);
    1452       27062 :     OGRFieldType eType = OFTString;
    1453       27062 :     OGRFieldSubType eSubType = OFSTNone;
    1454       27062 :     bool bTypeOK = true;
    1455             : 
    1456       27062 :     auto type = field->type();
    1457       27062 :     if (type->id() == arrow::Type::DICTIONARY && path.size() == 1)
    1458             :     {
    1459             :         const auto dictionaryType =
    1460         604 :             std::static_pointer_cast<arrow::DictionaryType>(field->type());
    1461         604 :         auto indexType = dictionaryType->index_type();
    1462         604 :         if (dictionaryType->value_type()->id() == arrow::Type::STRING &&
    1463         302 :             IsIntegerArrowType(indexType->id()))
    1464             :         {
    1465         302 :             if (bParquetColValid)
    1466             :             {
    1467         604 :                 std::string osDomainName(field->name() + "Domain");
    1468         302 :                 m_poDS->RegisterDomainName(osDomainName,
    1469         302 :                                            m_poFeatureDefn->GetFieldCount());
    1470         302 :                 oField.SetDomainName(osDomainName);
    1471             :             }
    1472         302 :             type = std::move(indexType);
    1473             :         }
    1474             :         else
    1475             :         {
    1476           0 :             bTypeOK = false;
    1477             :         }
    1478             :     }
    1479             : 
    1480       27062 :     int nParquetColIncrement = 1;
    1481       27062 :     switch (type->id())
    1482             :     {
    1483         671 :         case arrow::Type::STRUCT:
    1484             :         {
    1485        1342 :             const auto subfields = field->Flatten();
    1486             :             const std::string osExtensionName =
    1487        1342 :                 GetFieldExtensionName(field, type, GetDriverUCName().c_str());
    1488           5 :             if (osExtensionName == EXTENSION_NAME_ARROW_TIMESTAMP_WITH_OFFSET &&
    1489          10 :                 subfields.size() == 2 &&
    1490           5 :                 subfields[0]->name() ==
    1491         681 :                     field->name() + "." + ATSWO_TIMESTAMP_FIELD_NAME &&
    1492          10 :                 subfields[0]->type()->id() == arrow::Type::TIMESTAMP &&
    1493           5 :                 subfields[1]->name() ==
    1494         681 :                     field->name() + "." + ATSWO_OFFSET_MINUTES_FIELD_NAME &&
    1495           5 :                 subfields[1]->type()->id() == arrow::Type::INT16)
    1496             :             {
    1497           5 :                 oField.SetType(OFTDateTime);
    1498           5 :                 oField.SetTZFlag(OGR_TZFLAG_MIXED_TZ);
    1499           5 :                 oField.SetNullable(field->nullable());
    1500           5 :                 m_poFeatureDefn->AddFieldDefn(&oField);
    1501           5 :                 m_anMapFieldIndexToArrowColumn.push_back(path);
    1502           5 :                 m_apoArrowDataTypes.push_back(std::move(type));
    1503             :             }
    1504             :             else
    1505             :             {
    1506        1332 :                 auto newpath = path;
    1507         666 :                 newpath.push_back(0);
    1508        3018 :                 for (int j = 0; j < static_cast<int>(subfields.size()); j++)
    1509             :                 {
    1510        2352 :                     const auto &subfield = subfields[j];
    1511        2352 :                     bParquetColValid = CheckMatchArrowParquetColumnNames(
    1512             :                         iParquetCol, subfield);
    1513        2352 :                     if (!bParquetColValid)
    1514           0 :                         m_bHasMissingMappingToParquet = true;
    1515        2352 :                     newpath.back() = j;
    1516        2352 :                     CreateFieldFromSchema(subfield, bParquetColValid,
    1517             :                                           iParquetCol, newpath,
    1518             :                                           oMapFieldNameToGDALSchemaFieldDefn);
    1519             :                 }
    1520             :             }
    1521         671 :             return;  // return intended, not break
    1522             :         }
    1523             : 
    1524        5353 :         case arrow::Type::MAP:
    1525             :         {
    1526             :             // A arrow map maps to 2 Parquet columns
    1527        5353 :             nParquetColIncrement = 2;
    1528        5353 :             break;
    1529             :         }
    1530             : 
    1531       21038 :         default:
    1532       21038 :             break;
    1533             :     }
    1534             : 
    1535       26391 :     if (bTypeOK)
    1536             :     {
    1537       26391 :         bTypeOK = MapArrowTypeToOGR(type, field, oField, eType, eSubType, path,
    1538             :                                     oMapFieldNameToGDALSchemaFieldDefn);
    1539       26391 :         if (bTypeOK)
    1540             :         {
    1541       26101 :             m_apoArrowDataTypes.push_back(std::move(type));
    1542             :         }
    1543             :     }
    1544             : 
    1545       26391 :     if (bParquetColValid)
    1546       26391 :         iParquetCol += nParquetColIncrement;
    1547             : }
    1548             : 
    1549             : /************************************************************************/
    1550             : /*                            BuildDomain()                             */
    1551             : /************************************************************************/
    1552             : 
    1553             : std::unique_ptr<OGRFieldDomain>
    1554          16 : OGRParquetLayer::BuildDomain(const std::string &osDomainName,
    1555             :                              int iFieldIndex) const
    1556             : {
    1557          16 :     const int iArrowCol = m_anMapFieldIndexToArrowColumn[iFieldIndex][0];
    1558          32 :     const std::string osArrowColName = m_poSchema->fields()[iArrowCol]->name();
    1559          16 :     CPLAssert(m_poSchema->fields()[iArrowCol]->type()->id() ==
    1560             :               arrow::Type::DICTIONARY);
    1561             :     const auto anParquetColsForField =
    1562          48 :         GetParquetColumnIndicesForArrowField(osArrowColName.c_str());
    1563          16 :     CPLAssert(!anParquetColsForField.empty());
    1564          16 :     const auto oldBatchSize = m_poArrowReader->properties().batch_size();
    1565          16 :     m_poArrowReader->set_batch_size(1);
    1566             : #if PARQUET_VERSION_MAJOR >= 21
    1567             :     std::unique_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1568             :     auto result =
    1569             :         m_poArrowReader->GetRecordBatchReader({0}, anParquetColsForField);
    1570             :     if (result.ok())
    1571             :         poRecordBatchReader = std::move(*result);
    1572             : #else
    1573          16 :     std::shared_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1574          16 :     CPL_IGNORE_RET_VAL(m_poArrowReader->GetRecordBatchReader(
    1575             :         {0}, anParquetColsForField, &poRecordBatchReader));
    1576             : #endif
    1577          16 :     if (poRecordBatchReader != nullptr)
    1578             :     {
    1579           0 :         std::shared_ptr<arrow::RecordBatch> poBatch;
    1580          16 :         auto status = poRecordBatchReader->ReadNext(&poBatch);
    1581          16 :         if (!status.ok())
    1582             :         {
    1583           0 :             CPLError(CE_Failure, CPLE_AppDefined, "ReadNext() failed: %s",
    1584           0 :                      status.message().c_str());
    1585             :         }
    1586          16 :         else if (poBatch)
    1587             :         {
    1588          16 :             m_poArrowReader->set_batch_size(oldBatchSize);
    1589          16 :             return BuildDomainFromBatch(osDomainName, poBatch, 0);
    1590             :         }
    1591             :     }
    1592           0 :     m_poArrowReader->set_batch_size(oldBatchSize);
    1593           0 :     return nullptr;
    1594             : }
    1595             : 
    1596             : /************************************************************************/
    1597             : /*                     ComputeGeometryColumnType()                      */
    1598             : /************************************************************************/
    1599             : 
    1600             : OGRwkbGeometryType
    1601         297 : OGRParquetLayer::ComputeGeometryColumnType(int iGeomCol, int iParquetCol) const
    1602             : {
    1603             :     // Compute type of geometry column by iterating over each geometry, and
    1604             :     // looking at the WKB geometry type in the first 5 bytes of each geometry.
    1605             : 
    1606         297 :     OGRwkbGeometryType eGeomType = wkbNone;
    1607             : 
    1608         594 :     std::vector<int> anRowGroups;
    1609         297 :     const int nNumGroups = m_poArrowReader->num_row_groups();
    1610         297 :     anRowGroups.reserve(nNumGroups);
    1611         884 :     for (int i = 0; i < nNumGroups; ++i)
    1612         587 :         anRowGroups.push_back(i);
    1613             : #if PARQUET_VERSION_MAJOR >= 21
    1614             :     std::unique_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1615             :     auto result =
    1616             :         m_poArrowReader->GetRecordBatchReader(anRowGroups, {iParquetCol});
    1617             :     if (result.ok())
    1618             :         poRecordBatchReader = std::move(*result);
    1619             : #else
    1620           0 :     std::shared_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1621         297 :     CPL_IGNORE_RET_VAL(m_poArrowReader->GetRecordBatchReader(
    1622             :         anRowGroups, {iParquetCol}, &poRecordBatchReader));
    1623             : #endif
    1624         297 :     if (poRecordBatchReader != nullptr)
    1625             :     {
    1626         594 :         std::shared_ptr<arrow::RecordBatch> poBatch;
    1627             :         while (true)
    1628             :         {
    1629         596 :             auto status = poRecordBatchReader->ReadNext(&poBatch);
    1630         596 :             if (!status.ok())
    1631             :             {
    1632           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "ReadNext() failed: %s",
    1633           0 :                          status.message().c_str());
    1634           0 :                 break;
    1635             :             }
    1636         596 :             else if (!poBatch)
    1637         295 :                 break;
    1638             : 
    1639         301 :             eGeomType = ComputeGeometryColumnTypeProcessBatch(poBatch, iGeomCol,
    1640             :                                                               0, eGeomType);
    1641         301 :             if (eGeomType == wkbUnknown)
    1642           2 :                 break;
    1643         299 :         }
    1644             :     }
    1645             : 
    1646         594 :     return eGeomType == wkbNone ? wkbUnknown : eGeomType;
    1647             : }
    1648             : 
    1649             : /************************************************************************/
    1650             : /*                       GetFeatureExplicitFID()                        */
    1651             : /************************************************************************/
    1652             : 
    1653           4 : OGRFeature *OGRParquetLayer::GetFeatureExplicitFID(GIntBig nFID)
    1654             : {
    1655           8 :     std::vector<int> anRowGroups;
    1656           4 :     const int nNumGroups = m_poArrowReader->num_row_groups();
    1657           4 :     anRowGroups.reserve(nNumGroups);
    1658          16 :     for (int i = 0; i < nNumGroups; ++i)
    1659          12 :         anRowGroups.push_back(i);
    1660             : #if PARQUET_VERSION_MAJOR >= 21
    1661             :     std::unique_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1662             :     auto result = m_bIgnoredFields
    1663             :                       ? m_poArrowReader->GetRecordBatchReader(
    1664             :                             anRowGroups, m_anRequestedParquetColumns)
    1665             :                       : m_poArrowReader->GetRecordBatchReader(anRowGroups);
    1666             :     if (result.ok())
    1667             :     {
    1668             :         poRecordBatchReader = std::move(*result);
    1669             :     }
    1670             : #else
    1671           4 :     std::shared_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1672           4 :     if (m_bIgnoredFields)
    1673             :     {
    1674           4 :         CPL_IGNORE_RET_VAL(m_poArrowReader->GetRecordBatchReader(
    1675           2 :             anRowGroups, m_anRequestedParquetColumns, &poRecordBatchReader));
    1676             :     }
    1677             :     else
    1678             :     {
    1679           2 :         CPL_IGNORE_RET_VAL(m_poArrowReader->GetRecordBatchReader(
    1680             :             anRowGroups, &poRecordBatchReader));
    1681             :     }
    1682             : #endif
    1683           4 :     if (poRecordBatchReader != nullptr)
    1684             :     {
    1685           4 :         std::shared_ptr<arrow::RecordBatch> poBatch;
    1686             :         while (true)
    1687             :         {
    1688          14 :             auto status = poRecordBatchReader->ReadNext(&poBatch);
    1689          14 :             if (!status.ok())
    1690             :             {
    1691           0 :                 CPLError(CE_Failure, CPLE_AppDefined, "ReadNext() failed: %s",
    1692           0 :                          status.message().c_str());
    1693           0 :                 break;
    1694             :             }
    1695          14 :             else if (!poBatch)
    1696           2 :                 break;
    1697             : 
    1698          12 :             const auto array = poBatch->column(
    1699          12 :                 m_bIgnoredFields ? m_nRequestedFIDColumn : m_iFIDArrowColumn);
    1700          12 :             const auto arrayPtr = array.get();
    1701          12 :             const auto arrayTypeId = array->type_id();
    1702          30 :             for (int64_t nIdxInBatch = 0; nIdxInBatch < poBatch->num_rows();
    1703             :                  nIdxInBatch++)
    1704             :             {
    1705          20 :                 if (!array->IsNull(nIdxInBatch))
    1706             :                 {
    1707          20 :                     if (arrayTypeId == arrow::Type::INT64)
    1708             :                     {
    1709          20 :                         const auto castArray =
    1710             :                             static_cast<const arrow::Int64Array *>(arrayPtr);
    1711          20 :                         if (castArray->Value(nIdxInBatch) == nFID)
    1712             :                         {
    1713           2 :                             return ReadFeature(nIdxInBatch, poBatch->columns());
    1714             :                         }
    1715             :                     }
    1716           0 :                     else if (arrayTypeId == arrow::Type::INT32)
    1717             :                     {
    1718           0 :                         const auto castArray =
    1719             :                             static_cast<const arrow::Int32Array *>(arrayPtr);
    1720           0 :                         if (castArray->Value(nIdxInBatch) == nFID)
    1721             :                         {
    1722           0 :                             return ReadFeature(nIdxInBatch, poBatch->columns());
    1723             :                         }
    1724             :                     }
    1725             :                 }
    1726             :             }
    1727          10 :         }
    1728             :     }
    1729           2 :     return nullptr;
    1730             : }
    1731             : 
    1732             : /************************************************************************/
    1733             : /*                         GetFeatureByIndex()                          */
    1734             : /************************************************************************/
    1735             : 
    1736          64 : OGRFeature *OGRParquetLayer::GetFeatureByIndex(GIntBig nFID)
    1737             : {
    1738             : 
    1739          64 :     if (nFID < 0)
    1740           5 :         return nullptr;
    1741             : 
    1742         118 :     const auto metadata = m_poArrowReader->parquet_reader()->metadata();
    1743          59 :     const int nNumGroups = m_poArrowReader->num_row_groups();
    1744          59 :     int64_t nAccRows = 0;
    1745          72 :     for (int iGroup = 0; iGroup < nNumGroups; ++iGroup)
    1746             :     {
    1747             :         const int64_t nNextAccRows =
    1748          63 :             nAccRows + metadata->RowGroup(iGroup)->num_rows();
    1749          63 :         if (nFID < nNextAccRows)
    1750             :         {
    1751             : #if PARQUET_VERSION_MAJOR >= 21
    1752             :             std::unique_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1753             :             auto result = m_bIgnoredFields
    1754             :                               ? m_poArrowReader->GetRecordBatchReader(
    1755             :                                     {iGroup}, m_anRequestedParquetColumns)
    1756             :                               : m_poArrowReader->GetRecordBatchReader({iGroup});
    1757             :             if (result.ok())
    1758             :             {
    1759             :                 poRecordBatchReader = std::move(*result);
    1760             :             }
    1761             :             else
    1762             :             {
    1763             :                 CPLError(CE_Failure, CPLE_AppDefined,
    1764             :                          "GetRecordBatchReader() failed: %s",
    1765             :                          result.status().message().c_str());
    1766             :                 return nullptr;
    1767             :             }
    1768             : #else
    1769          50 :             std::shared_ptr<arrow::RecordBatchReader> poRecordBatchReader;
    1770             :             {
    1771           0 :                 arrow::Status status;
    1772          50 :                 if (m_bIgnoredFields)
    1773             :                 {
    1774           0 :                     status = m_poArrowReader->GetRecordBatchReader(
    1775           0 :                         {iGroup}, m_anRequestedParquetColumns,
    1776           0 :                         &poRecordBatchReader);
    1777             :                 }
    1778             :                 else
    1779             :                 {
    1780         100 :                     status = m_poArrowReader->GetRecordBatchReader(
    1781          50 :                         {iGroup}, &poRecordBatchReader);
    1782             :                 }
    1783          50 :                 if (poRecordBatchReader == nullptr)
    1784             :                 {
    1785           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
    1786             :                              "GetRecordBatchReader() failed: %s",
    1787           0 :                              status.message().c_str());
    1788           0 :                     return nullptr;
    1789             :                 }
    1790             :             }
    1791             : #endif
    1792             : 
    1793          50 :             const int64_t nExpectedIdxInGroup = nFID - nAccRows;
    1794          50 :             int64_t nIdxInGroup = 0;
    1795             :             while (true)
    1796             :             {
    1797           0 :                 std::shared_ptr<arrow::RecordBatch> poBatch;
    1798          50 :                 arrow::Status status = poRecordBatchReader->ReadNext(&poBatch);
    1799          50 :                 if (!status.ok())
    1800             :                 {
    1801           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
    1802           0 :                              "ReadNext() failed: %s", status.message().c_str());
    1803           0 :                     return nullptr;
    1804             :                 }
    1805          50 :                 if (poBatch == nullptr)
    1806             :                 {
    1807           0 :                     return nullptr;
    1808             :                 }
    1809          50 :                 if (nExpectedIdxInGroup < nIdxInGroup + poBatch->num_rows())
    1810             :                 {
    1811          50 :                     const auto nIdxInBatch = nExpectedIdxInGroup - nIdxInGroup;
    1812             :                     auto poFeature =
    1813          50 :                         ReadFeature(nIdxInBatch, poBatch->columns());
    1814          50 :                     poFeature->SetFID(nFID);
    1815          50 :                     return poFeature;
    1816             :                 }
    1817           0 :                 nIdxInGroup += poBatch->num_rows();
    1818           0 :             }
    1819             :         }
    1820          13 :         nAccRows = nNextAccRows;
    1821             :     }
    1822           9 :     return nullptr;
    1823             : }
    1824             : 
    1825             : /************************************************************************/
    1826             : /*                             GetFeature()                             */
    1827             : /************************************************************************/
    1828             : 
    1829          68 : OGRFeature *OGRParquetLayer::GetFeature(GIntBig nFID)
    1830             : {
    1831          68 :     if (!m_osFIDColumn.empty())
    1832             :     {
    1833           4 :         return GetFeatureExplicitFID(nFID);
    1834             :     }
    1835             :     else
    1836             :     {
    1837          64 :         return GetFeatureByIndex(nFID);
    1838             :     }
    1839             : }
    1840             : 
    1841             : /************************************************************************/
    1842             : /*                            ResetReading()                            */
    1843             : /************************************************************************/
    1844             : 
    1845        4561 : void OGRParquetLayer::ResetReading()
    1846             : {
    1847        4561 :     OGRParquetLayerBase::ResetReading();
    1848        4561 :     m_oFeatureIdxRemappingIter = m_asFeatureIdxRemapping.begin();
    1849        4561 :     m_nFeatureIdxSelected = 0;
    1850        4561 :     if (!m_asFeatureIdxRemapping.empty())
    1851             :     {
    1852        2202 :         m_nFeatureIdx = m_oFeatureIdxRemappingIter->second;
    1853        2202 :         ++m_oFeatureIdxRemappingIter;
    1854             :     }
    1855        4561 : }
    1856             : 
    1857             : /************************************************************************/
    1858             : /*                      CreateRecordBatchReader()                       */
    1859             : /************************************************************************/
    1860             : 
    1861         689 : bool OGRParquetLayer::CreateRecordBatchReader(int iStartingRowGroup)
    1862             : {
    1863        1378 :     std::vector<int> anRowGroups;
    1864         689 :     const int nNumGroups = m_poArrowReader->num_row_groups();
    1865         689 :     anRowGroups.reserve(nNumGroups - iStartingRowGroup);
    1866        1731 :     for (int i = iStartingRowGroup; i < nNumGroups; ++i)
    1867        1042 :         anRowGroups.push_back(i);
    1868        1378 :     return CreateRecordBatchReader(anRowGroups);
    1869             : }
    1870             : 
    1871         989 : bool OGRParquetLayer::CreateRecordBatchReader(
    1872             :     const std::vector<int> &anRowGroups)
    1873             : {
    1874             : #if PARQUET_VERSION_MAJOR >= 21
    1875             :     auto result = m_bIgnoredFields
    1876             :                       ? m_poArrowReader->GetRecordBatchReader(
    1877             :                             anRowGroups, m_anRequestedParquetColumns)
    1878             :                       : m_poArrowReader->GetRecordBatchReader(anRowGroups);
    1879             :     if (result.ok())
    1880             :     {
    1881             :         m_poRecordBatchReader = std::move(*result);
    1882             :         return true;
    1883             :     }
    1884             :     else
    1885             :     {
    1886             :         CPLError(CE_Failure, CPLE_AppDefined,
    1887             :                  "GetRecordBatchReader() failed: %s",
    1888             :                  result.status().message().c_str());
    1889             :         return false;
    1890             :     }
    1891             : #else
    1892         989 :     arrow::Status status;
    1893         989 :     if (m_bIgnoredFields)
    1894             :     {
    1895         470 :         status = m_poArrowReader->GetRecordBatchReader(
    1896         235 :             anRowGroups, m_anRequestedParquetColumns, &m_poRecordBatchReader);
    1897             :     }
    1898             :     else
    1899             :     {
    1900        1508 :         status = m_poArrowReader->GetRecordBatchReader(anRowGroups,
    1901         754 :                                                        &m_poRecordBatchReader);
    1902             :     }
    1903         989 :     if (m_poRecordBatchReader == nullptr)
    1904             :     {
    1905           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    1906           0 :                  "GetRecordBatchReader() failed: %s", status.message().c_str());
    1907           0 :         return false;
    1908             :     }
    1909         989 :     return true;
    1910             : #endif
    1911             : }
    1912             : 
    1913             : /************************************************************************/
    1914             : /*                        IsConstraintPossible()                        */
    1915             : /************************************************************************/
    1916             : 
    1917             : enum class IsConstraintPossibleRes
    1918             : {
    1919             :     YES,
    1920             :     NO,
    1921             :     UNKNOWN
    1922             : };
    1923             : 
    1924             : template <class T>
    1925         224 : static IsConstraintPossibleRes IsConstraintPossible(int nOperation, T v, T min,
    1926             :                                                     T max)
    1927             : {
    1928         224 :     if (nOperation == SWQ_EQ)
    1929             :     {
    1930         146 :         if (v < min || v > max)
    1931             :         {
    1932          59 :             return IsConstraintPossibleRes::NO;
    1933             :         }
    1934             :     }
    1935          78 :     else if (nOperation == SWQ_NE)
    1936             :     {
    1937          38 :         if (v == min && v == max)
    1938             :         {
    1939           0 :             return IsConstraintPossibleRes::NO;
    1940             :         }
    1941             :     }
    1942          40 :     else if (nOperation == SWQ_LE)
    1943             :     {
    1944          10 :         if (v < min)
    1945             :         {
    1946           4 :             return IsConstraintPossibleRes::NO;
    1947             :         }
    1948             :     }
    1949          30 :     else if (nOperation == SWQ_LT)
    1950             :     {
    1951          10 :         if (v <= min)
    1952             :         {
    1953           4 :             return IsConstraintPossibleRes::NO;
    1954             :         }
    1955             :     }
    1956          20 :     else if (nOperation == SWQ_GE)
    1957             :     {
    1958          10 :         if (v > max)
    1959             :         {
    1960           4 :             return IsConstraintPossibleRes::NO;
    1961             :         }
    1962             :     }
    1963          10 :     else if (nOperation == SWQ_GT)
    1964             :     {
    1965          10 :         if (v >= max)
    1966             :         {
    1967           6 :             return IsConstraintPossibleRes::NO;
    1968             :         }
    1969             :     }
    1970             :     else
    1971             :     {
    1972           0 :         CPLDebug("PARQUET",
    1973             :                  "IsConstraintPossible: Unhandled operation type: %d",
    1974             :                  nOperation);
    1975           0 :         return IsConstraintPossibleRes::UNKNOWN;
    1976             :     }
    1977         147 :     return IsConstraintPossibleRes::YES;
    1978             : }
    1979             : 
    1980             : /************************************************************************/
    1981             : /*                           IncrFeatureIdx()                           */
    1982             : /************************************************************************/
    1983             : 
    1984        8180 : void OGRParquetLayer::IncrFeatureIdx()
    1985             : {
    1986        8180 :     ++m_nFeatureIdxSelected;
    1987        8180 :     ++m_nFeatureIdx;
    1988        9335 :     if (m_iFIDArrowColumn < 0 && !m_asFeatureIdxRemapping.empty() &&
    1989        9335 :         m_oFeatureIdxRemappingIter != m_asFeatureIdxRemapping.end())
    1990             :     {
    1991         140 :         if (m_nFeatureIdxSelected == m_oFeatureIdxRemappingIter->first)
    1992             :         {
    1993          48 :             m_nFeatureIdx = m_oFeatureIdxRemappingIter->second;
    1994          48 :             ++m_oFeatureIdxRemappingIter;
    1995             :         }
    1996             :     }
    1997        8180 : }
    1998             : 
    1999             : /************************************************************************/
    2000             : /*                           ReadNextBatch()                            */
    2001             : /************************************************************************/
    2002             : 
    2003        2115 : bool OGRParquetLayer::ReadNextBatch()
    2004             : {
    2005        2115 :     m_nIdxInBatch = 0;
    2006             : 
    2007        2115 :     const int nNumGroups = m_poArrowReader->num_row_groups();
    2008        2115 :     if (nNumGroups == 0)
    2009           2 :         return false;
    2010             : 
    2011        2113 :     if (m_bSingleBatch)
    2012             :     {
    2013          32 :         CPLAssert(m_iRecordBatch == 0);
    2014          32 :         CPLAssert(m_poBatch != nullptr);
    2015          32 :         return false;
    2016             :     }
    2017             : 
    2018        2081 :     CPLAssert((m_iRecordBatch == -1 && m_poRecordBatchReader == nullptr) ||
    2019             :               (m_iRecordBatch >= 0 && m_poRecordBatchReader != nullptr));
    2020             : 
    2021        2081 :     if (m_poRecordBatchReader == nullptr)
    2022             :     {
    2023         995 :         m_asFeatureIdxRemapping.clear();
    2024             : 
    2025         995 :         bool bIterateEverything = false;
    2026         995 :         std::vector<int> anSelectedGroups;
    2027             :         const auto oIterToGeomColBBOX =
    2028         995 :             m_oMapGeomFieldIndexToGeomColBBOXParquet.find(m_iGeomFieldFilter);
    2029             :         const bool bUSEBBOXFields =
    2030         243 :             (m_poFilterGeom &&
    2031         243 :              oIterToGeomColBBOX !=
    2032        1238 :                  m_oMapGeomFieldIndexToGeomColBBOXParquet.end() &&
    2033         141 :              CPLTestBool(CPLGetConfigOption(
    2034        1136 :                  ("OGR_" + GetDriverUCName() + "_USE_BBOX").c_str(), "YES")));
    2035             :         const bool bIsGeoArrowStruct =
    2036        1990 :             (m_iGeomFieldFilter >= 0 &&
    2037         995 :              m_iGeomFieldFilter < static_cast<int>(m_aeGeomEncoding.size()) &&
    2038         985 :              m_iGeomFieldFilter <
    2039             :                  static_cast<int>(
    2040        1970 :                      m_anMapGeomFieldIndexToParquetColumns.size()) &&
    2041         985 :              m_anMapGeomFieldIndexToParquetColumns[m_iGeomFieldFilter].size() >=
    2042        1990 :                  2 &&
    2043         306 :              OGRArrowIsGeoArrowStruct(m_aeGeomEncoding[m_iGeomFieldFilter]));
    2044             : #if PARQUET_VERSION_MAJOR >= 21
    2045             :         const bool bUseParquetGeoStat =
    2046             :             (m_poFilterGeom && m_iGeomFieldFilter >= 0 &&
    2047             :              m_geoStatsWithBBOXAvailable.find(m_iGeomFieldFilter) !=
    2048             :                  m_geoStatsWithBBOXAvailable.end() &&
    2049             :              m_iGeomFieldFilter <
    2050             :                  static_cast<int>(
    2051             :                      m_anMapGeomFieldIndexToParquetColumns.size()) &&
    2052             :              m_anMapGeomFieldIndexToParquetColumns[m_iGeomFieldFilter].size() ==
    2053             :                  1 &&
    2054             :              m_anMapGeomFieldIndexToParquetColumns[m_iGeomFieldFilter][0] >= 0);
    2055             : #endif
    2056        1716 :         if (m_asAttributeFilterConstraints.empty() && !bUSEBBOXFields &&
    2057         721 :             !(bIsGeoArrowStruct && m_poFilterGeom)
    2058             : #if PARQUET_VERSION_MAJOR >= 21
    2059             :             && !bUseParquetGeoStat
    2060             : #endif
    2061             :         )
    2062             :         {
    2063         675 :             bIterateEverything = true;
    2064             :         }
    2065             :         else
    2066             :         {
    2067             :             OGRField sMin;
    2068             :             OGRField sMax;
    2069         320 :             OGR_RawField_SetNull(&sMin);
    2070         320 :             OGR_RawField_SetNull(&sMax);
    2071         320 :             bool bFoundMin = false;
    2072         320 :             bool bFoundMax = false;
    2073         320 :             OGRFieldType eType = OFTMaxType;
    2074         320 :             OGRFieldSubType eSubType = OFSTNone;
    2075         640 :             std::string osMinTmp, osMaxTmp;
    2076         320 :             int64_t nFeatureIdxSelected = 0;
    2077         320 :             int64_t nFeatureIdxTotal = 0;
    2078             : 
    2079         320 :             int iXMinField = -1;
    2080         320 :             int iYMinField = -1;
    2081         320 :             int iXMaxField = -1;
    2082         320 :             int iYMaxField = -1;
    2083             : 
    2084         320 :             if (bIsGeoArrowStruct)
    2085             :             {
    2086             :                 const auto metadata =
    2087         276 :                     m_poArrowReader->parquet_reader()->metadata();
    2088         138 :                 const auto poParquetSchema = metadata->schema();
    2089         342 :                 for (int iParquetCol :
    2090         822 :                      m_anMapGeomFieldIndexToParquetColumns[m_iGeomFieldFilter])
    2091             :                 {
    2092             :                     const auto parquetColumn =
    2093         342 :                         poParquetSchema->Column(iParquetCol);
    2094             :                     const auto parquetColumnName =
    2095         684 :                         parquetColumn->path()->ToDotString();
    2096         684 :                     if (parquetColumnName.size() > 2 &&
    2097         342 :                         parquetColumnName.find(".x") ==
    2098         342 :                             parquetColumnName.size() - 2)
    2099             :                     {
    2100         138 :                         iXMinField = iParquetCol;
    2101         138 :                         iXMaxField = iParquetCol;
    2102             :                     }
    2103         408 :                     else if (parquetColumnName.size() > 2 &&
    2104         204 :                              parquetColumnName.find(".y") ==
    2105         204 :                                  parquetColumnName.size() - 2)
    2106             :                     {
    2107         138 :                         iYMinField = iParquetCol;
    2108         138 :                         iYMaxField = iParquetCol;
    2109             :                     }
    2110             :                 }
    2111             :             }
    2112         182 :             else if (bUSEBBOXFields)
    2113             :             {
    2114          49 :                 iXMinField = oIterToGeomColBBOX->second.iParquetXMin;
    2115          49 :                 iYMinField = oIterToGeomColBBOX->second.iParquetYMin;
    2116          49 :                 iXMaxField = oIterToGeomColBBOX->second.iParquetXMax;
    2117          49 :                 iYMaxField = oIterToGeomColBBOX->second.iParquetYMax;
    2118             :             }
    2119             : 
    2120         765 :             for (int iRowGroup = 0;
    2121         765 :                  iRowGroup < nNumGroups && !bIterateEverything; ++iRowGroup)
    2122             :             {
    2123         445 :                 bool bSelectGroup = true;
    2124             :                 auto poRowGroup =
    2125         445 :                     GetReader()->parquet_reader()->RowGroup(iRowGroup);
    2126             : 
    2127         445 :                 if (iXMinField >= 0 && iYMinField >= 0 && iXMaxField >= 0 &&
    2128             :                     iYMaxField >= 0)
    2129             :                 {
    2130         195 :                     if (GetMinMaxForParquetCol(iRowGroup, iXMinField, nullptr,
    2131             :                                                true, sMin, bFoundMin, false,
    2132             :                                                sMax, bFoundMax, eType, eSubType,
    2133         194 :                                                osMinTmp, osMaxTmp) &&
    2134         389 :                         bFoundMin && eType == OFTReal)
    2135             :                     {
    2136         194 :                         const double dfGroupMinX = sMin.Real;
    2137         194 :                         if (dfGroupMinX > m_sFilterEnvelope.MaxX)
    2138             :                         {
    2139           1 :                             bSelectGroup = false;
    2140             :                         }
    2141         193 :                         else if (GetMinMaxForParquetCol(
    2142             :                                      iRowGroup, iYMinField, nullptr, true, sMin,
    2143             :                                      bFoundMin, false, sMax, bFoundMax, eType,
    2144         193 :                                      eSubType, osMinTmp, osMaxTmp) &&
    2145         386 :                                  bFoundMin && eType == OFTReal)
    2146             :                         {
    2147         193 :                             const double dfGroupMinY = sMin.Real;
    2148         193 :                             if (dfGroupMinY > m_sFilterEnvelope.MaxY)
    2149             :                             {
    2150           1 :                                 bSelectGroup = false;
    2151             :                             }
    2152         192 :                             else if (GetMinMaxForParquetCol(
    2153             :                                          iRowGroup, iXMaxField, nullptr, false,
    2154             :                                          sMin, bFoundMin, true, sMax, bFoundMax,
    2155         192 :                                          eType, eSubType, osMinTmp, osMaxTmp) &&
    2156         384 :                                      bFoundMax && eType == OFTReal)
    2157             :                             {
    2158         192 :                                 const double dfGroupMaxX = sMax.Real;
    2159         192 :                                 if (dfGroupMaxX < m_sFilterEnvelope.MinX)
    2160             :                                 {
    2161           1 :                                     bSelectGroup = false;
    2162             :                                 }
    2163         191 :                                 else if (GetMinMaxForParquetCol(
    2164             :                                              iRowGroup, iYMaxField, nullptr,
    2165             :                                              false, sMin, bFoundMin, true, sMax,
    2166             :                                              bFoundMax, eType, eSubType,
    2167         191 :                                              osMinTmp, osMaxTmp) &&
    2168         382 :                                          bFoundMax && eType == OFTReal)
    2169             :                                 {
    2170         191 :                                     const double dfGroupMaxY = sMax.Real;
    2171         191 :                                     if (dfGroupMaxY < m_sFilterEnvelope.MinY)
    2172             :                                     {
    2173           1 :                                         bSelectGroup = false;
    2174             :                                     }
    2175             :                                 }
    2176             :                             }
    2177             :                         }
    2178             :                     }
    2179             :                 }
    2180             : #if PARQUET_VERSION_MAJOR >= 21
    2181             :                 else if (bUseParquetGeoStat)
    2182             :                 {
    2183             :                     const int iParquetCol =
    2184             :                         m_anMapGeomFieldIndexToParquetColumns
    2185             :                             [m_iGeomFieldFilter][0];
    2186             :                     CPLAssert(iParquetCol >= 0);
    2187             : 
    2188             :                     const auto metadata =
    2189             :                         m_poArrowReader->parquet_reader()->metadata();
    2190             :                     const auto columnChunk =
    2191             :                         metadata->RowGroup(iRowGroup)->ColumnChunk(iParquetCol);
    2192             :                     if (auto geostats = columnChunk->geo_statistics())
    2193             :                     {
    2194             :                         if (geostats->dimension_valid()[0] &&
    2195             :                             geostats->dimension_valid()[1])
    2196             :                         {
    2197             :                             double dfMinX = geostats->lower_bound()[0];
    2198             :                             double dfMaxX = geostats->upper_bound()[0];
    2199             :                             double dfMinY = geostats->lower_bound()[1];
    2200             :                             double dfMaxY = geostats->upper_bound()[1];
    2201             : 
    2202             :                             // Deal as best as we can with wrap around bounding box
    2203             :                             if (dfMinX > dfMaxX && std::fabs(dfMinX) <= 180 &&
    2204             :                                 std::fabs(dfMaxX) <= 180)
    2205             :                             {
    2206             :                                 dfMinX = -180;
    2207             :                                 dfMaxX = 180;
    2208             :                             }
    2209             : 
    2210             :                             // Check if there is an intersection between
    2211             :                             // the geostatistics for this rowgroup and
    2212             :                             // the bbox of interest
    2213             :                             if (dfMinX > m_sFilterEnvelope.MaxX ||
    2214             :                                 dfMaxX < m_sFilterEnvelope.MinX ||
    2215             :                                 dfMinY > m_sFilterEnvelope.MaxY ||
    2216             :                                 dfMaxY < m_sFilterEnvelope.MinY)
    2217             :                             {
    2218             :                                 bSelectGroup = false;
    2219             :                             }
    2220             :                         }
    2221             :                     }
    2222             :                 }
    2223             : #endif
    2224             : 
    2225         445 :                 if (bSelectGroup)
    2226             :                 {
    2227         604 :                     for (auto &constraint : m_asAttributeFilterConstraints)
    2228             :                     {
    2229         254 :                         int iOGRField = constraint.iField;
    2230         508 :                         if (constraint.iField ==
    2231         254 :                             m_poFeatureDefn->GetFieldCount() + SPF_FID)
    2232             :                         {
    2233           9 :                             iOGRField = OGR_FID_INDEX;
    2234             :                         }
    2235         254 :                         if (constraint.nOperation != SWQ_ISNULL &&
    2236         245 :                             constraint.nOperation != SWQ_ISNOTNULL)
    2237             :                         {
    2238         232 :                             if (iOGRField == OGR_FID_INDEX &&
    2239           9 :                                 m_iFIDParquetColumn < 0)
    2240             :                             {
    2241           6 :                                 sMin.Integer64 = nFeatureIdxTotal;
    2242           6 :                                 sMax.Integer64 =
    2243           6 :                                     nFeatureIdxTotal +
    2244           6 :                                     poRowGroup->metadata()->num_rows() - 1;
    2245           6 :                                 eType = OFTInteger64;
    2246             :                             }
    2247         226 :                             else if (!GetMinMaxForOGRField(
    2248             :                                          iRowGroup, iOGRField, true, sMin,
    2249             :                                          bFoundMin, true, sMax, bFoundMax,
    2250         221 :                                          eType, eSubType, osMinTmp, osMaxTmp) ||
    2251         226 :                                      !bFoundMin || !bFoundMax)
    2252             :                             {
    2253           5 :                                 bIterateEverything = true;
    2254           5 :                                 break;
    2255             :                             }
    2256             :                         }
    2257             : 
    2258         249 :                         IsConstraintPossibleRes res =
    2259             :                             IsConstraintPossibleRes::UNKNOWN;
    2260         249 :                         if (constraint.eType ==
    2261         147 :                                 OGRArrowLayer::Constraint::Type::Integer &&
    2262         147 :                             eType == OFTInteger)
    2263             :                         {
    2264             : #if 0
    2265             :                             CPLDebug("PARQUET",
    2266             :                                      "Group %d, field %s, min = %d, max = %d",
    2267             :                                      iRowGroup,
    2268             :                                      iOGRField == OGR_FID_INDEX
    2269             :                                          ? m_osFIDColumn.c_str()
    2270             :                                          : m_poFeatureDefn->GetFieldDefn(iOGRField)
    2271             :                                                ->GetNameRef(),
    2272             :                                      sMin.Integer, sMax.Integer);
    2273             : #endif
    2274         125 :                             res = IsConstraintPossible(
    2275             :                                 constraint.nOperation,
    2276             :                                 constraint.sValue.Integer, sMin.Integer,
    2277             :                                 sMax.Integer);
    2278             :                         }
    2279         124 :                         else if (constraint.eType == OGRArrowLayer::Constraint::
    2280          35 :                                                          Type::Integer64 &&
    2281          35 :                                  eType == OFTInteger64)
    2282             :                         {
    2283             : #if 0
    2284             :                             CPLDebug("PARQUET",
    2285             :                                      "Group %d, field %s, min = " CPL_FRMT_GIB
    2286             :                                      ", max = " CPL_FRMT_GIB,
    2287             :                                      iRowGroup,
    2288             :                                      iOGRField == OGR_FID_INDEX
    2289             :                                          ? m_osFIDColumn.c_str()
    2290             :                                          : m_poFeatureDefn->GetFieldDefn(iOGRField)
    2291             :                                                ->GetNameRef(),
    2292             :                                      static_cast<GIntBig>(sMin.Integer64),
    2293             :                                      static_cast<GIntBig>(sMax.Integer64));
    2294             : #endif
    2295          35 :                             res = IsConstraintPossible(
    2296             :                                 constraint.nOperation,
    2297             :                                 constraint.sValue.Integer64, sMin.Integer64,
    2298             :                                 sMax.Integer64);
    2299             :                         }
    2300          89 :                         else if (constraint.eType ==
    2301          29 :                                      OGRArrowLayer::Constraint::Type::Real &&
    2302          29 :                                  eType == OFTReal)
    2303             :                         {
    2304             : #if 0
    2305             :                             CPLDebug("PARQUET",
    2306             :                                      "Group %d, field %s, min = %g, max = %g",
    2307             :                                      iRowGroup,
    2308             :                                      iOGRField == OGR_FID_INDEX
    2309             :                                          ? m_osFIDColumn.c_str()
    2310             :                                          : m_poFeatureDefn->GetFieldDefn(iOGRField)
    2311             :                                                ->GetNameRef(),
    2312             :                                      sMin.Real, sMax.Real);
    2313             : #endif
    2314          26 :                             res = IsConstraintPossible(constraint.nOperation,
    2315             :                                                        constraint.sValue.Real,
    2316             :                                                        sMin.Real, sMax.Real);
    2317             :                         }
    2318          63 :                         else if (constraint.eType ==
    2319          38 :                                      OGRArrowLayer::Constraint::Type::String &&
    2320          38 :                                  eType == OFTString)
    2321             :                         {
    2322             : #if 0
    2323             :                             CPLDebug("PARQUET",
    2324             :                                      "Group %d, field %s, min = %s, max = %s",
    2325             :                                      iRowGroup,
    2326             :                                      iOGRField == OGR_FID_INDEX
    2327             :                                          ? m_osFIDColumn.c_str()
    2328             :                                          : m_poFeatureDefn->GetFieldDefn(iOGRField)
    2329             :                                                ->GetNameRef(),
    2330             :                                      sMin.String, sMax.String);
    2331             : #endif
    2332          38 :                             res = IsConstraintPossible(
    2333             :                                 constraint.nOperation,
    2334          76 :                                 std::string(constraint.sValue.String),
    2335          76 :                                 std::string(sMin.String),
    2336          76 :                                 std::string(sMax.String));
    2337             :                         }
    2338          25 :                         else if (constraint.nOperation == SWQ_ISNULL ||
    2339          16 :                                  constraint.nOperation == SWQ_ISNOTNULL)
    2340             :                         {
    2341             :                             const std::vector<int> anCols =
    2342             :                                 iOGRField == OGR_FID_INDEX
    2343           0 :                                     ? std::vector<int>{m_iFIDParquetColumn}
    2344             :                                     : GetParquetColumnIndicesForArrowField(
    2345          44 :                                           GetLayerDefn()
    2346          22 :                                               ->GetFieldDefn(iOGRField)
    2347          88 :                                               ->GetNameRef());
    2348          22 :                             if (anCols.size() == 1 && anCols[0] >= 0)
    2349             :                             {
    2350             :                                 const auto metadata =
    2351          22 :                                     m_poArrowReader->parquet_reader()
    2352          44 :                                         ->metadata();
    2353             :                                 const auto rowGroupColumnChunk =
    2354          22 :                                     metadata->RowGroup(iRowGroup)->ColumnChunk(
    2355          44 :                                         anCols[0]);
    2356             :                                 const auto rowGroupStats =
    2357          44 :                                     rowGroupColumnChunk->statistics();
    2358          44 :                                 if (rowGroupColumnChunk->is_stats_set() &&
    2359          22 :                                     rowGroupStats)
    2360             :                                 {
    2361          22 :                                     res = IsConstraintPossibleRes::YES;
    2362          31 :                                     if (constraint.nOperation == SWQ_ISNULL &&
    2363           9 :                                         rowGroupStats->num_values() ==
    2364           9 :                                             poRowGroup->metadata()->num_rows())
    2365             :                                     {
    2366           5 :                                         res = IsConstraintPossibleRes::NO;
    2367             :                                     }
    2368          34 :                                     else if (constraint.nOperation ==
    2369          30 :                                                  SWQ_ISNOTNULL &&
    2370          13 :                                              rowGroupStats->num_values() == 0)
    2371             :                                     {
    2372           1 :                                         res = IsConstraintPossibleRes::NO;
    2373             :                                     }
    2374             :                                 }
    2375          22 :                             }
    2376             :                         }
    2377             :                         else
    2378             :                         {
    2379           3 :                             CPLDebug(
    2380             :                                 "PARQUET",
    2381             :                                 "Unhandled combination of constraint.eType "
    2382             :                                 "(%d) and eType (%d)",
    2383           3 :                                 static_cast<int>(constraint.eType), eType);
    2384             :                         }
    2385             : 
    2386         249 :                         if (res == IsConstraintPossibleRes::NO)
    2387             :                         {
    2388          83 :                             bSelectGroup = false;
    2389          83 :                             break;
    2390             :                         }
    2391         166 :                         else if (res == IsConstraintPossibleRes::UNKNOWN)
    2392             :                         {
    2393           3 :                             bIterateEverything = true;
    2394           3 :                             break;
    2395             :                         }
    2396             :                     }
    2397             :                 }
    2398             : 
    2399         445 :                 if (bSelectGroup)
    2400             :                 {
    2401             :                     // CPLDebug("PARQUET", "Selecting row group %d", iRowGroup);
    2402             :                     m_asFeatureIdxRemapping.emplace_back(
    2403         358 :                         std::make_pair(nFeatureIdxSelected, nFeatureIdxTotal));
    2404         358 :                     anSelectedGroups.push_back(iRowGroup);
    2405         358 :                     nFeatureIdxSelected += poRowGroup->metadata()->num_rows();
    2406             :                 }
    2407             : 
    2408         445 :                 nFeatureIdxTotal += poRowGroup->metadata()->num_rows();
    2409             :             }
    2410             :         }
    2411             : 
    2412         995 :         if (bIterateEverything)
    2413             :         {
    2414         683 :             m_asFeatureIdxRemapping.clear();
    2415         683 :             m_oFeatureIdxRemappingIter = m_asFeatureIdxRemapping.begin();
    2416         683 :             if (!CreateRecordBatchReader(0))
    2417           0 :                 return false;
    2418             :         }
    2419             :         else
    2420             :         {
    2421         312 :             m_oFeatureIdxRemappingIter = m_asFeatureIdxRemapping.begin();
    2422         312 :             if (anSelectedGroups.empty())
    2423             :             {
    2424          12 :                 return false;
    2425             :             }
    2426         300 :             CPLDebug("PARQUET", "%d/%d row groups selected",
    2427         300 :                      int(anSelectedGroups.size()),
    2428         300 :                      m_poArrowReader->num_row_groups());
    2429         300 :             m_nFeatureIdx = m_oFeatureIdxRemappingIter->second;
    2430         300 :             ++m_oFeatureIdxRemappingIter;
    2431         300 :             if (!CreateRecordBatchReader(anSelectedGroups))
    2432             :             {
    2433           0 :                 return false;
    2434             :             }
    2435             :         }
    2436             :     }
    2437             : 
    2438        4138 :     std::shared_ptr<arrow::RecordBatch> poNextBatch;
    2439             : 
    2440           0 :     do
    2441             :     {
    2442        2069 :         ++m_iRecordBatch;
    2443        2069 :         poNextBatch.reset();
    2444        2069 :         auto status = m_poRecordBatchReader->ReadNext(&poNextBatch);
    2445        2069 :         if (!status.ok())
    2446             :         {
    2447           0 :             CPLError(CE_Failure, CPLE_AppDefined, "ReadNext() failed: %s",
    2448           0 :                      status.message().c_str());
    2449           0 :             poNextBatch.reset();
    2450             :         }
    2451        2069 :         if (poNextBatch == nullptr)
    2452             :         {
    2453        1113 :             if (m_iRecordBatch == 1 && m_poBatch && m_poAttrQuery == nullptr &&
    2454         365 :                 m_poFilterGeom == nullptr)
    2455             :             {
    2456          59 :                 m_iRecordBatch = 0;
    2457          59 :                 m_bSingleBatch = true;
    2458             :             }
    2459             :             else
    2460         689 :                 m_poBatch.reset();
    2461         748 :             return false;
    2462             :         }
    2463        1321 :     } while (poNextBatch->num_rows() == 0);
    2464             : 
    2465        1321 :     SetBatch(poNextBatch);
    2466             : 
    2467        1321 :     return true;
    2468             : }
    2469             : 
    2470             : /************************************************************************/
    2471             : /*                      InvalidateCachedBatches()                       */
    2472             : /************************************************************************/
    2473             : 
    2474         953 : void OGRParquetLayer::InvalidateCachedBatches()
    2475             : {
    2476         953 :     m_bSingleBatch = false;
    2477         953 :     OGRParquetLayerBase::InvalidateCachedBatches();
    2478         953 : }
    2479             : 
    2480             : /************************************************************************/
    2481             : /*                          SetIgnoredFields()                          */
    2482             : /************************************************************************/
    2483             : 
    2484         260 : OGRErr OGRParquetLayer::SetIgnoredFields(CSLConstList papszFields)
    2485             : {
    2486         260 :     m_bIgnoredFields = false;
    2487         260 :     m_anRequestedParquetColumns.clear();
    2488         260 :     m_anMapFieldIndexToArrayIndex.clear();
    2489         260 :     m_anMapGeomFieldIndexToArrayIndex.clear();
    2490         260 :     m_nRequestedFIDColumn = -1;
    2491         260 :     OGRErr eErr = OGRLayer::SetIgnoredFields(papszFields);
    2492         260 :     int nBatchColumns = 0;
    2493         260 :     if (!m_bHasMissingMappingToParquet && eErr == OGRERR_NONE)
    2494             :     {
    2495         260 :         m_bIgnoredFields = papszFields != nullptr && papszFields[0] != nullptr;
    2496         260 :         if (m_bIgnoredFields)
    2497             :         {
    2498         197 :             if (m_iFIDParquetColumn >= 0)
    2499             :             {
    2500           6 :                 m_nRequestedFIDColumn = nBatchColumns;
    2501           6 :                 nBatchColumns++;
    2502           6 :                 m_anRequestedParquetColumns.push_back(m_iFIDParquetColumn);
    2503             :             }
    2504             : 
    2505        5971 :             for (int i = 0; i < m_poFeatureDefn->GetFieldCount(); ++i)
    2506             :             {
    2507             :                 const auto eArrowType =
    2508        5774 :                     m_poSchema->fields()[m_anMapFieldIndexToArrowColumn[i][0]]
    2509        5774 :                         ->type()
    2510        5774 :                         ->id();
    2511        5774 :                 if (eArrowType == arrow::Type::STRUCT)
    2512             :                 {
    2513             :                     // For a struct, for the sake of simplicity in
    2514             :                     // GetNextRawFeature(), as soon as one of the member if
    2515             :                     // requested, request all Parquet columns, so that the Arrow
    2516             :                     // type doesn't change
    2517          69 :                     bool bFoundNotIgnored = false;
    2518         296 :                     for (int j = i; j < m_poFeatureDefn->GetFieldCount() &&
    2519         294 :                                     m_anMapFieldIndexToArrowColumn[i][0] ==
    2520         147 :                                         m_anMapFieldIndexToArrowColumn[j][0];
    2521             :                          ++j)
    2522             :                     {
    2523         136 :                         if (!m_poFeatureDefn->GetFieldDefn(j)->IsIgnored())
    2524             :                         {
    2525          56 :                             bFoundNotIgnored = true;
    2526          56 :                             break;
    2527             :                         }
    2528             :                     }
    2529          69 :                     if (bFoundNotIgnored)
    2530             :                     {
    2531             :                         int j;
    2532         784 :                         for (j = i; j < m_poFeatureDefn->GetFieldCount() &&
    2533         784 :                                     m_anMapFieldIndexToArrowColumn[i][0] ==
    2534         392 :                                         m_anMapFieldIndexToArrowColumn[j][0];
    2535             :                              ++j)
    2536             :                         {
    2537         336 :                             if (!m_poFeatureDefn->GetFieldDefn(j)->IsIgnored())
    2538             :                             {
    2539         330 :                                 m_anMapFieldIndexToArrayIndex.push_back(
    2540             :                                     nBatchColumns);
    2541             :                             }
    2542             :                             else
    2543             :                             {
    2544           6 :                                 m_anMapFieldIndexToArrayIndex.push_back(-1);
    2545             :                             }
    2546             : 
    2547             :                             const int iArrowCol =
    2548         336 :                                 m_anMapFieldIndexToArrowColumn[i][0];
    2549             :                             const std::string osArrowColName =
    2550         672 :                                 m_poSchema->fields()[iArrowCol]->name();
    2551             :                             const auto anParquetColsForField =
    2552             :                                 GetParquetColumnIndicesForArrowField(
    2553         672 :                                     osArrowColName.c_str());
    2554             :                             m_anRequestedParquetColumns.insert(
    2555         336 :                                 m_anRequestedParquetColumns.end(),
    2556             :                                 anParquetColsForField.begin(),
    2557         672 :                                 anParquetColsForField.end());
    2558             :                         }
    2559          56 :                         i = j - 1;
    2560          56 :                         nBatchColumns++;
    2561             :                     }
    2562             :                     else
    2563             :                     {
    2564             :                         int j;
    2565         172 :                         for (j = i; j < m_poFeatureDefn->GetFieldCount() &&
    2566         170 :                                     m_anMapFieldIndexToArrowColumn[i][0] ==
    2567          85 :                                         m_anMapFieldIndexToArrowColumn[j][0];
    2568             :                              ++j)
    2569             :                         {
    2570          74 :                             m_anMapFieldIndexToArrayIndex.push_back(-1);
    2571             :                         }
    2572          13 :                         i = j - 1;
    2573             :                     }
    2574             :                 }
    2575        5705 :                 else if (!m_poFeatureDefn->GetFieldDefn(i)->IsIgnored())
    2576             :                 {
    2577        4183 :                     m_anMapFieldIndexToArrayIndex.push_back(nBatchColumns);
    2578        4183 :                     nBatchColumns++;
    2579        4183 :                     const int iArrowCol = m_anMapFieldIndexToArrowColumn[i][0];
    2580             :                     const std::string osArrowColName =
    2581        8366 :                         m_poSchema->fields()[iArrowCol]->name();
    2582             :                     const auto anParquetColsForField =
    2583        4183 :                         GetParquetColumnIndicesForArrowField(osArrowColName);
    2584             :                     m_anRequestedParquetColumns.insert(
    2585        4183 :                         m_anRequestedParquetColumns.end(),
    2586             :                         anParquetColsForField.begin(),
    2587        8366 :                         anParquetColsForField.end());
    2588             :                 }
    2589             :                 else
    2590             :                 {
    2591        1522 :                     m_anMapFieldIndexToArrayIndex.push_back(-1);
    2592             :                 }
    2593             :             }
    2594             : 
    2595         197 :             CPLAssert(static_cast<int>(m_anMapFieldIndexToArrayIndex.size()) ==
    2596             :                       m_poFeatureDefn->GetFieldCount());
    2597             : 
    2598         406 :             for (int i = 0; i < m_poFeatureDefn->GetGeomFieldCount(); ++i)
    2599             :             {
    2600         209 :                 if (!m_poFeatureDefn->GetGeomFieldDefn(i)->IsIgnored())
    2601             :                 {
    2602             :                     const auto &anVals =
    2603         185 :                         m_anMapGeomFieldIndexToParquetColumns[i];
    2604         185 :                     CPLAssert(!anVals.empty() && anVals[0] >= 0);
    2605             :                     m_anRequestedParquetColumns.insert(
    2606         185 :                         m_anRequestedParquetColumns.end(), anVals.begin(),
    2607         370 :                         anVals.end());
    2608         185 :                     m_anMapGeomFieldIndexToArrayIndex.push_back(nBatchColumns);
    2609         185 :                     nBatchColumns++;
    2610             : 
    2611         185 :                     auto oIter = m_oMapGeomFieldIndexToGeomColBBOX.find(i);
    2612             :                     const auto oIterParquet =
    2613         185 :                         m_oMapGeomFieldIndexToGeomColBBOXParquet.find(i);
    2614         275 :                     if (oIter != m_oMapGeomFieldIndexToGeomColBBOX.end() &&
    2615          90 :                         oIterParquet !=
    2616         275 :                             m_oMapGeomFieldIndexToGeomColBBOXParquet.end())
    2617             :                     {
    2618          90 :                         oIter->second.iArrayIdx = nBatchColumns++;
    2619             :                         m_anRequestedParquetColumns.insert(
    2620          90 :                             m_anRequestedParquetColumns.end(),
    2621          90 :                             oIterParquet->second.anParquetCols.begin(),
    2622         270 :                             oIterParquet->second.anParquetCols.end());
    2623             :                     }
    2624             :                 }
    2625             :                 else
    2626             :                 {
    2627          24 :                     m_anMapGeomFieldIndexToArrayIndex.push_back(-1);
    2628             :                 }
    2629             :             }
    2630             : 
    2631         197 :             CPLAssert(
    2632             :                 static_cast<int>(m_anMapGeomFieldIndexToArrayIndex.size()) ==
    2633             :                 m_poFeatureDefn->GetGeomFieldCount());
    2634             :         }
    2635             :     }
    2636             : 
    2637         260 :     m_nExpectedBatchColumns = m_bIgnoredFields ? nBatchColumns : -1;
    2638             : 
    2639         260 :     ComputeConstraintsArrayIdx();
    2640             : 
    2641             :     // Full invalidation
    2642         260 :     InvalidateCachedBatches();
    2643             : 
    2644         260 :     return eErr;
    2645             : }
    2646             : 
    2647             : /************************************************************************/
    2648             : /*                          GetFeatureCount()                           */
    2649             : /************************************************************************/
    2650             : 
    2651        1057 : GIntBig OGRParquetLayer::GetFeatureCount(int bForce)
    2652             : {
    2653        1057 :     if (m_poAttrQuery == nullptr && m_poFilterGeom == nullptr)
    2654             :     {
    2655          55 :         auto metadata = m_poArrowReader->parquet_reader()->metadata();
    2656          55 :         if (metadata)
    2657          55 :             return metadata->num_rows();
    2658             :     }
    2659        1002 :     return OGRLayer::GetFeatureCount(bForce);
    2660             : }
    2661             : 
    2662             : /************************************************************************/
    2663             : /*                           FastGetExtent()                            */
    2664             : /************************************************************************/
    2665             : 
    2666         833 : bool OGRParquetLayer::FastGetExtent(int iGeomField, OGREnvelope *psExtent) const
    2667             : {
    2668         833 :     if (OGRParquetLayerBase::FastGetExtent(iGeomField, psExtent))
    2669         818 :         return true;
    2670             : 
    2671             :     const auto oIterToGeomColBBOX =
    2672          15 :         m_oMapGeomFieldIndexToGeomColBBOXParquet.find(iGeomField);
    2673          16 :     if (oIterToGeomColBBOX != m_oMapGeomFieldIndexToGeomColBBOXParquet.end() &&
    2674           1 :         CPLTestBool(CPLGetConfigOption("OGR_PARQUET_USE_BBOX", "YES")))
    2675             :     {
    2676           1 :         OGREnvelope sExtent;
    2677             :         OGRField sMin, sMax;
    2678           1 :         OGR_RawField_SetNull(&sMin);
    2679           1 :         OGR_RawField_SetNull(&sMax);
    2680             :         bool bFoundMin, bFoundMax;
    2681           1 :         OGRFieldType eType = OFTMaxType;
    2682           1 :         OGRFieldSubType eSubType = OFSTNone;
    2683           1 :         std::string osMinTmp, osMaxTmp;
    2684           2 :         if (GetMinMaxForParquetCol(-1, oIterToGeomColBBOX->second.iParquetXMin,
    2685             :                                    nullptr, true, sMin, bFoundMin, false, sMax,
    2686             :                                    bFoundMax, eType, eSubType, osMinTmp,
    2687           3 :                                    osMaxTmp) &&
    2688           1 :             eType == OFTReal)
    2689             :         {
    2690           1 :             sExtent.MinX = sMin.Real;
    2691             : 
    2692           1 :             if (GetMinMaxForParquetCol(
    2693           1 :                     -1, oIterToGeomColBBOX->second.iParquetYMin, nullptr, true,
    2694             :                     sMin, bFoundMin, false, sMax, bFoundMax, eType, eSubType,
    2695           3 :                     osMinTmp, osMaxTmp) &&
    2696           1 :                 eType == OFTReal)
    2697             :             {
    2698           1 :                 sExtent.MinY = sMin.Real;
    2699             : 
    2700           1 :                 if (GetMinMaxForParquetCol(
    2701           1 :                         -1, oIterToGeomColBBOX->second.iParquetXMax, nullptr,
    2702             :                         false, sMin, bFoundMin, true, sMax, bFoundMax, eType,
    2703           3 :                         eSubType, osMinTmp, osMaxTmp) &&
    2704           1 :                     eType == OFTReal)
    2705             :                 {
    2706           1 :                     sExtent.MaxX = sMax.Real;
    2707             : 
    2708           1 :                     if (GetMinMaxForParquetCol(
    2709           1 :                             -1, oIterToGeomColBBOX->second.iParquetYMax,
    2710             :                             nullptr, false, sMin, bFoundMin, true, sMax,
    2711           3 :                             bFoundMax, eType, eSubType, osMinTmp, osMaxTmp) &&
    2712           1 :                         eType == OFTReal)
    2713             :                     {
    2714           1 :                         sExtent.MaxY = sMax.Real;
    2715             : 
    2716           1 :                         CPLDebug("PARQUET",
    2717             :                                  "Using statistics of bbox.minx, bbox.miny, "
    2718             :                                  "bbox.maxx, bbox.maxy columns to get extent");
    2719           1 :                         m_oMapExtents[iGeomField] = sExtent;
    2720           1 :                         *psExtent = sExtent;
    2721           1 :                         return true;
    2722             :                     }
    2723             :                 }
    2724             :             }
    2725             :         }
    2726             :     }
    2727             : 
    2728          14 :     return false;
    2729             : }
    2730             : 
    2731             : /************************************************************************/
    2732             : /*                           TestCapability()                           */
    2733             : /************************************************************************/
    2734             : 
    2735         687 : int OGRParquetLayer::TestCapability(const char *pszCap) const
    2736             : {
    2737         687 :     if (EQUAL(pszCap, OLCFastFeatureCount))
    2738          79 :         return m_poAttrQuery == nullptr && m_poFilterGeom == nullptr;
    2739             : 
    2740         608 :     if (EQUAL(pszCap, OLCIgnoreFields))
    2741           9 :         return !m_bHasMissingMappingToParquet;
    2742             : 
    2743         599 :     if (EQUAL(pszCap, OLCFastSpatialFilter))
    2744             :     {
    2745         252 :         if (m_iGeomFieldFilter >= 0 &&
    2746         168 :             m_iGeomFieldFilter < static_cast<int>(m_aeGeomEncoding.size()) &&
    2747          84 :             OGRArrowIsGeoArrowStruct(m_aeGeomEncoding[m_iGeomFieldFilter]))
    2748             :         {
    2749          84 :             return true;
    2750             :         }
    2751             : 
    2752             : #if PARQUET_VERSION_MAJOR >= 21
    2753             :         if (m_iGeomFieldFilter >= 0 &&
    2754             :             m_iGeomFieldFilter < static_cast<int>(m_aeGeomEncoding.size()) &&
    2755             :             m_aeGeomEncoding[m_iGeomFieldFilter] == OGRArrowGeomEncoding::WKB &&
    2756             :             m_iGeomFieldFilter <
    2757             :                 static_cast<int>(
    2758             :                     m_anMapGeomFieldIndexToParquetColumns.size()) &&
    2759             :             m_anMapGeomFieldIndexToParquetColumns[m_iGeomFieldFilter].size() ==
    2760             :                 1)
    2761             :         {
    2762             :             const int iParquetCol =
    2763             :                 m_anMapGeomFieldIndexToParquetColumns[m_iGeomFieldFilter][0];
    2764             :             if (iParquetCol >= 0)
    2765             :             {
    2766             :                 const auto metadata =
    2767             :                     m_poArrowReader->parquet_reader()->metadata();
    2768             : 
    2769             :                 int nCountRowGroupsStatsValid = 0;
    2770             :                 const int nNumGroups = m_poArrowReader->num_row_groups();
    2771             :                 for (int iRowGroup = 0; iRowGroup < nNumGroups &&
    2772             :                                         nCountRowGroupsStatsValid == iRowGroup;
    2773             :                      ++iRowGroup)
    2774             :                 {
    2775             :                     const auto columnChunk =
    2776             :                         metadata->RowGroup(iRowGroup)->ColumnChunk(iParquetCol);
    2777             :                     if (auto geostats = columnChunk->geo_statistics())
    2778             :                     {
    2779             :                         if (geostats->dimension_valid()[0] &&
    2780             :                             geostats->dimension_valid()[1])
    2781             :                         {
    2782             :                             const double dfMinX = geostats->lower_bound()[0];
    2783             :                             const double dfMaxX = geostats->upper_bound()[0];
    2784             :                             const double dfMinY = geostats->lower_bound()[1];
    2785             :                             const double dfMaxY = geostats->upper_bound()[1];
    2786             :                             if (std::isfinite(dfMinX) &&
    2787             :                                 std::isfinite(dfMaxX) &&
    2788             :                                 std::isfinite(dfMinY) && std::isfinite(dfMaxY))
    2789             :                             {
    2790             :                                 nCountRowGroupsStatsValid++;
    2791             :                             }
    2792             :                         }
    2793             :                     }
    2794             :                 }
    2795             :                 if (nCountRowGroupsStatsValid == nNumGroups)
    2796             :                 {
    2797             :                     return true;
    2798             :                 }
    2799             :             }
    2800             :         }
    2801             : #endif
    2802             : 
    2803             :         // fallback to base method
    2804             :     }
    2805             : 
    2806         515 :     return OGRParquetLayerBase::TestCapability(pszCap);
    2807             : }
    2808             : 
    2809             : /************************************************************************/
    2810             : /*                          GetMetadataItem()                           */
    2811             : /************************************************************************/
    2812             : 
    2813         510 : const char *OGRParquetLayer::GetMetadataItem(const char *pszName,
    2814             :                                              const char *pszDomain)
    2815             : {
    2816             :     // Mostly for unit test purposes
    2817         510 :     if (pszDomain != nullptr && EQUAL(pszDomain, "_PARQUET_"))
    2818             :     {
    2819          11 :         int nRowGroupIdx = -1;
    2820          11 :         int nColumn = -1;
    2821          11 :         if (EQUAL(pszName, "NUM_ROW_GROUPS"))
    2822             :         {
    2823           3 :             return CPLSPrintf("%d", m_poArrowReader->num_row_groups());
    2824             :         }
    2825           8 :         if (EQUAL(pszName, "CREATOR"))
    2826             :         {
    2827           4 :             return CPLSPrintf("%s", m_poArrowReader->parquet_reader()
    2828           4 :                                         ->metadata()
    2829           2 :                                         ->created_by()
    2830           2 :                                         .c_str());
    2831             :         }
    2832          12 :         else if (sscanf(pszName, "ROW_GROUPS[%d]", &nRowGroupIdx) == 1 &&
    2833           6 :                  strstr(pszName, ".NUM_ROWS"))
    2834             :         {
    2835             :             try
    2836             :             {
    2837             :                 auto poRowGroup =
    2838           6 :                     m_poArrowReader->parquet_reader()->RowGroup(nRowGroupIdx);
    2839           3 :                 if (poRowGroup == nullptr)
    2840           0 :                     return nullptr;
    2841           3 :                 return CPLSPrintf("%" PRId64,
    2842           3 :                                   poRowGroup->metadata()->num_rows());
    2843             :             }
    2844           0 :             catch (const std::exception &)
    2845             :             {
    2846             :             }
    2847             :         }
    2848           6 :         else if (sscanf(pszName, "ROW_GROUPS[%d].COLUMNS[%d]", &nRowGroupIdx,
    2849           6 :                         &nColumn) == 2 &&
    2850           3 :                  strstr(pszName, ".COMPRESSION"))
    2851             :         {
    2852             :             try
    2853             :             {
    2854             :                 auto poRowGroup =
    2855           6 :                     m_poArrowReader->parquet_reader()->RowGroup(nRowGroupIdx);
    2856           3 :                 if (poRowGroup == nullptr)
    2857           0 :                     return nullptr;
    2858           6 :                 auto poColumn = poRowGroup->metadata()->ColumnChunk(nColumn);
    2859           3 :                 return CPLSPrintf("%s", arrow::util::Codec::GetCodecAsString(
    2860           3 :                                             poColumn->compression())
    2861           3 :                                             .c_str());
    2862             :             }
    2863           0 :             catch (const std::exception &)
    2864             :             {
    2865             :             }
    2866             :         }
    2867           0 :         return nullptr;
    2868             :     }
    2869         499 :     if (pszDomain != nullptr && EQUAL(pszDomain, "_PARQUET_METADATA_"))
    2870             :     {
    2871         628 :         const auto metadata = m_poArrowReader->parquet_reader()->metadata();
    2872         314 :         const auto &kv_metadata = metadata->key_value_metadata();
    2873         314 :         if (kv_metadata && kv_metadata->Contains(pszName))
    2874             :         {
    2875         311 :             auto metadataItem = kv_metadata->Get(pszName);
    2876         311 :             if (metadataItem.ok())
    2877             :             {
    2878         311 :                 return CPLSPrintf("%s", metadataItem->c_str());
    2879             :             }
    2880             :         }
    2881           3 :         return nullptr;
    2882             :     }
    2883         185 :     return OGRLayer::GetMetadataItem(pszName, pszDomain);
    2884             : }
    2885             : 
    2886             : /************************************************************************/
    2887             : /*                            GetMetadata()                             */
    2888             : /************************************************************************/
    2889             : 
    2890          61 : CSLConstList OGRParquetLayer::GetMetadata(const char *pszDomain)
    2891             : {
    2892             :     // Mostly for unit test purposes
    2893          61 :     if (pszDomain != nullptr && EQUAL(pszDomain, "_PARQUET_METADATA_"))
    2894             :     {
    2895           2 :         m_aosFeatherMetadata.Clear();
    2896           4 :         const auto metadata = m_poArrowReader->parquet_reader()->metadata();
    2897           2 :         const auto &kv_metadata = metadata->key_value_metadata();
    2898           2 :         if (kv_metadata)
    2899             :         {
    2900           8 :             for (const auto &kv : kv_metadata->sorted_pairs())
    2901             :             {
    2902             :                 m_aosFeatherMetadata.SetNameValue(kv.first.c_str(),
    2903           6 :                                                   kv.second.c_str());
    2904             :             }
    2905             :         }
    2906           2 :         return m_aosFeatherMetadata.List();
    2907             :     }
    2908             : 
    2909             :     // Mostly for unit test purposes
    2910          59 :     if (pszDomain != nullptr && EQUAL(pszDomain, "_GDAL_CREATION_OPTIONS_"))
    2911             :     {
    2912           6 :         return m_aosCreationOptions.List();
    2913             :     }
    2914             : 
    2915          53 :     return OGRLayer::GetMetadata(pszDomain);
    2916             : }
    2917             : 
    2918             : /************************************************************************/
    2919             : /*                           GetArrowStream()                           */
    2920             : /************************************************************************/
    2921             : 
    2922         135 : bool OGRParquetLayer::GetArrowStream(struct ArrowArrayStream *out_stream,
    2923             :                                      CSLConstList papszOptions)
    2924             : {
    2925             :     const char *pszMaxFeaturesInBatch =
    2926         135 :         CSLFetchNameValue(papszOptions, "MAX_FEATURES_IN_BATCH");
    2927         135 :     if (pszMaxFeaturesInBatch)
    2928             :     {
    2929          14 :         int nMaxBatchSize = atoi(pszMaxFeaturesInBatch);
    2930          14 :         if (nMaxBatchSize <= 0)
    2931           0 :             nMaxBatchSize = 1;
    2932          14 :         if (nMaxBatchSize > INT_MAX - 1)
    2933           0 :             nMaxBatchSize = INT_MAX - 1;
    2934          14 :         m_poArrowReader->set_batch_size(nMaxBatchSize);
    2935             :     }
    2936         135 :     return OGRArrowLayer::GetArrowStream(out_stream, papszOptions);
    2937             : }
    2938             : 
    2939             : /************************************************************************/
    2940             : /*                           SetNextByIndex()                           */
    2941             : /************************************************************************/
    2942             : 
    2943          14 : OGRErr OGRParquetLayer::SetNextByIndex(GIntBig nIndex)
    2944             : {
    2945          14 :     if (nIndex < 0)
    2946             :     {
    2947           4 :         m_bEOF = true;
    2948           4 :         return OGRERR_NON_EXISTING_FEATURE;
    2949             :     }
    2950             : 
    2951          20 :     const auto metadata = m_poArrowReader->parquet_reader()->metadata();
    2952          10 :     if (nIndex >= metadata->num_rows())
    2953             :     {
    2954           4 :         m_bEOF = true;
    2955           4 :         return OGRERR_NON_EXISTING_FEATURE;
    2956             :     }
    2957             : 
    2958           6 :     m_bEOF = false;
    2959             : 
    2960           6 :     if (m_bSingleBatch)
    2961             :     {
    2962           0 :         ResetReading();
    2963           0 :         m_nIdxInBatch = nIndex;
    2964           0 :         m_nFeatureIdx = nIndex;
    2965           0 :         return OGRERR_NONE;
    2966             :     }
    2967             : 
    2968           6 :     const int nNumGroups = m_poArrowReader->num_row_groups();
    2969           6 :     int64_t nAccRows = 0;
    2970           6 :     const auto nBatchSize = m_poArrowReader->properties().batch_size();
    2971           6 :     m_iRecordBatch = -1;
    2972           6 :     ResetReading();
    2973           6 :     m_iRecordBatch = 0;
    2974           7 :     for (int iGroup = 0; iGroup < nNumGroups; ++iGroup)
    2975             :     {
    2976           7 :         const auto nRowsInRowGroup = metadata->RowGroup(iGroup)->num_rows();
    2977           7 :         const int64_t nNextAccRows = nAccRows + nRowsInRowGroup;
    2978           7 :         if (nIndex < nNextAccRows)
    2979             :         {
    2980           6 :             if (!CreateRecordBatchReader(iGroup))
    2981           0 :                 return OGRERR_FAILURE;
    2982             : 
    2983          12 :             std::shared_ptr<arrow::RecordBatch> poBatch;
    2984             :             while (true)
    2985             :             {
    2986           6 :                 auto status = m_poRecordBatchReader->ReadNext(&poBatch);
    2987           6 :                 if (!status.ok())
    2988             :                 {
    2989           0 :                     CPLError(CE_Failure, CPLE_AppDefined,
    2990           0 :                              "ReadNext() failed: %s", status.message().c_str());
    2991           0 :                     m_iRecordBatch = -1;
    2992           0 :                     ResetReading();
    2993           0 :                     return OGRERR_FAILURE;
    2994             :                 }
    2995           6 :                 if (poBatch == nullptr)
    2996             :                 {
    2997           0 :                     m_iRecordBatch = -1;
    2998           0 :                     ResetReading();
    2999           0 :                     return OGRERR_FAILURE;
    3000             :                 }
    3001           6 :                 if (nIndex < nAccRows + poBatch->num_rows())
    3002             :                 {
    3003           6 :                     break;
    3004             :                 }
    3005           0 :                 nAccRows += poBatch->num_rows();
    3006           0 :                 m_iRecordBatch++;
    3007           0 :             }
    3008           6 :             m_nIdxInBatch = nIndex - nAccRows;
    3009           6 :             m_nFeatureIdx = nIndex;
    3010           6 :             SetBatch(poBatch);
    3011           6 :             return OGRERR_NONE;
    3012             :         }
    3013           1 :         nAccRows = nNextAccRows;
    3014           1 :         m_iRecordBatch +=
    3015           1 :             static_cast<int>(cpl::div_round_up(nRowsInRowGroup, nBatchSize));
    3016             :     }
    3017             : 
    3018           0 :     m_iRecordBatch = -1;
    3019           0 :     ResetReading();
    3020           0 :     return OGRERR_FAILURE;
    3021             : }
    3022             : 
    3023             : /************************************************************************/
    3024             : /*                              GetStats()                              */
    3025             : /************************************************************************/
    3026             : 
    3027             : template <class STAT_TYPE> struct GetStats
    3028             : {
    3029             :     using T = typename STAT_TYPE::T;
    3030             : 
    3031         609 :     static T min(const std::shared_ptr<parquet::FileMetaData> &metadata,
    3032             :                  const int iRowGroup, const int numRowGroups, const int iCol,
    3033             :                  bool &bFound)
    3034             :     {
    3035         609 :         T v{};
    3036         609 :         bFound = false;
    3037        1226 :         for (int i = 0; i < (iRowGroup < 0 ? numRowGroups : 1); i++)
    3038             :         {
    3039         653 :             const auto columnChunk =
    3040          30 :                 metadata->RowGroup(iRowGroup < 0 ? i : iRowGroup)
    3041             :                     ->ColumnChunk(iCol);
    3042         623 :             const auto colStats = columnChunk->statistics();
    3043        1243 :             if (columnChunk->is_stats_set() && colStats &&
    3044         620 :                 colStats->HasMinMax())
    3045             :             {
    3046         614 :                 auto castStats = static_cast<STAT_TYPE *>(colStats.get());
    3047         614 :                 const auto rowGroupVal = castStats->min();
    3048         614 :                 if (i == 0 || rowGroupVal < v)
    3049             :                 {
    3050         602 :                     bFound = true;
    3051         602 :                     v = rowGroupVal;
    3052             :                 }
    3053             :             }
    3054           9 :             else if (columnChunk->num_values() > 0)
    3055             :             {
    3056           6 :                 bFound = false;
    3057           6 :                 break;
    3058             :             }
    3059             :         }
    3060         609 :         return v;
    3061             :     }
    3062             : 
    3063         598 :     static T max(const std::shared_ptr<parquet::FileMetaData> &metadata,
    3064             :                  const int iRowGroup, const int numRowGroups, const int iCol,
    3065             :                  bool &bFound)
    3066             :     {
    3067         598 :         T v{};
    3068         598 :         bFound = false;
    3069        1210 :         for (int i = 0; i < (iRowGroup < 0 ? numRowGroups : 1); i++)
    3070             :         {
    3071         642 :             const auto columnChunk =
    3072          30 :                 metadata->RowGroup(iRowGroup < 0 ? i : iRowGroup)
    3073             :                     ->ColumnChunk(iCol);
    3074         612 :             const auto colStats = columnChunk->statistics();
    3075        1222 :             if (columnChunk->is_stats_set() && colStats &&
    3076         610 :                 colStats->HasMinMax())
    3077             :             {
    3078         610 :                 auto castStats = static_cast<STAT_TYPE *>(colStats.get());
    3079         610 :                 const auto rowGroupVal = castStats->max();
    3080         610 :                 if (i == 0 || rowGroupVal > v)
    3081             :                 {
    3082         608 :                     bFound = true;
    3083         608 :                     v = rowGroupVal;
    3084             :                 }
    3085             :             }
    3086           2 :             else if (columnChunk->num_values() > 0)
    3087             :             {
    3088           0 :                 bFound = false;
    3089           0 :                 break;
    3090             :             }
    3091             :         }
    3092         598 :         return v;
    3093             :     }
    3094             : };
    3095             : 
    3096             : template <> struct GetStats<parquet::ByteArrayStatistics>
    3097             : {
    3098             :     static std::string
    3099          39 :     min(const std::shared_ptr<parquet::FileMetaData> &metadata,
    3100             :         const int iRowGroup, const int numRowGroups, const int iCol,
    3101             :         bool &bFound)
    3102             :     {
    3103          39 :         std::string v{};
    3104          39 :         bFound = false;
    3105          79 :         for (int i = 0; i < (iRowGroup < 0 ? numRowGroups : 1); i++)
    3106             :         {
    3107             :             const auto columnChunk =
    3108          40 :                 metadata->RowGroup(iRowGroup < 0 ? i : iRowGroup)
    3109          80 :                     ->ColumnChunk(iCol);
    3110          80 :             const auto colStats = columnChunk->statistics();
    3111          80 :             if (columnChunk->is_stats_set() && colStats &&
    3112          40 :                 colStats->HasMinMax())
    3113             :             {
    3114             :                 auto castStats =
    3115          40 :                     static_cast<parquet::ByteArrayStatistics *>(colStats.get());
    3116          40 :                 const auto rowGroupValRaw = castStats->min();
    3117             :                 std::string rowGroupVal(
    3118          40 :                     reinterpret_cast<const char *>(rowGroupValRaw.ptr),
    3119          80 :                     rowGroupValRaw.len);
    3120          40 :                 if (i == 0 || rowGroupVal < v)
    3121             :                 {
    3122          39 :                     bFound = true;
    3123          39 :                     v = std::move(rowGroupVal);
    3124             :                 }
    3125             :             }
    3126             :         }
    3127          39 :         return v;
    3128             :     }
    3129             : 
    3130             :     static std::string
    3131          39 :     max(const std::shared_ptr<parquet::FileMetaData> &metadata,
    3132             :         const int iRowGroup, const int numRowGroups, const int iCol,
    3133             :         bool &bFound)
    3134             :     {
    3135          39 :         std::string v{};
    3136          39 :         bFound = false;
    3137          79 :         for (int i = 0; i < (iRowGroup < 0 ? numRowGroups : 1); i++)
    3138             :         {
    3139             :             const auto columnChunk =
    3140          40 :                 metadata->RowGroup(iRowGroup < 0 ? i : iRowGroup)
    3141          40 :                     ->ColumnChunk(iCol);
    3142          40 :             const auto colStats = columnChunk->statistics();
    3143          80 :             if (columnChunk->is_stats_set() && colStats &&
    3144          40 :                 colStats->HasMinMax())
    3145             :             {
    3146             :                 auto castStats =
    3147          40 :                     static_cast<parquet::ByteArrayStatistics *>(colStats.get());
    3148          40 :                 const auto rowGroupValRaw = castStats->max();
    3149             :                 std::string rowGroupVal(
    3150          40 :                     reinterpret_cast<const char *>(rowGroupValRaw.ptr),
    3151          80 :                     rowGroupValRaw.len);
    3152          40 :                 if (i == 0 || rowGroupVal > v)
    3153             :                 {
    3154          40 :                     bFound = true;
    3155          40 :                     v = std::move(rowGroupVal);
    3156             :                 }
    3157             :             }
    3158             :             else
    3159             :             {
    3160           0 :                 bFound = false;
    3161           0 :                 break;
    3162             :             }
    3163             :         }
    3164          39 :         return v;
    3165             :     }
    3166             : };
    3167             : 
    3168             : /************************************************************************/
    3169             : /*                        GetMinMaxForOGRField()                        */
    3170             : /************************************************************************/
    3171             : 
    3172         256 : bool OGRParquetLayer::GetMinMaxForOGRField(int iRowGroup,  // -1 for all
    3173             :                                            int iOGRField, bool bComputeMin,
    3174             :                                            OGRField &sMin, bool &bFoundMin,
    3175             :                                            bool bComputeMax, OGRField &sMax,
    3176             :                                            bool &bFoundMax, OGRFieldType &eType,
    3177             :                                            OGRFieldSubType &eSubType,
    3178             :                                            std::string &osMinTmp,
    3179             :                                            std::string &osMaxTmp) const
    3180             : {
    3181         256 :     OGR_RawField_SetNull(&sMin);
    3182         256 :     OGR_RawField_SetNull(&sMax);
    3183         256 :     eType = OFTReal;
    3184         256 :     eSubType = OFSTNone;
    3185         256 :     bFoundMin = false;
    3186         256 :     bFoundMax = false;
    3187             : 
    3188             :     const std::vector<int> anCols =
    3189             :         iOGRField == OGR_FID_INDEX
    3190           5 :             ? std::vector<int>{m_iFIDParquetColumn}
    3191             :             : GetParquetColumnIndicesForArrowField(
    3192        1019 :                   GetLayerDefn()->GetFieldDefn(iOGRField)->GetNameRef());
    3193         256 :     if (anCols.empty() || anCols[0] < 0)
    3194           2 :         return false;
    3195         254 :     const int iCol = anCols[0];
    3196             :     const auto &arrowType = iOGRField == OGR_FID_INDEX
    3197         254 :                                 ? m_poFIDType
    3198         249 :                                 : GetArrowFieldTypes()[iOGRField];
    3199             : 
    3200         254 :     const bool bRet = GetMinMaxForParquetCol(
    3201             :         iRowGroup, iCol, arrowType, bComputeMin, sMin, bFoundMin, bComputeMax,
    3202             :         sMax, bFoundMax, eType, eSubType, osMinTmp, osMaxTmp);
    3203             : 
    3204         254 :     if (eType == OFTInteger64 && arrowType->id() == arrow::Type::TIMESTAMP)
    3205             :     {
    3206             :         const OGRFieldDefn oDummyFIDFieldDefn(m_osFIDColumn.c_str(),
    3207           4 :                                               OFTInteger64);
    3208             :         const OGRFieldDefn *poFieldDefn =
    3209           2 :             iOGRField == OGR_FID_INDEX ? &oDummyFIDFieldDefn
    3210             :                                        : const_cast<OGRParquetLayer *>(this)
    3211           2 :                                              ->GetLayerDefn()
    3212           2 :                                              ->GetFieldDefn(iOGRField);
    3213           2 :         if (poFieldDefn->GetType() == OFTDateTime)
    3214             :         {
    3215             :             const auto timestampType =
    3216           2 :                 static_cast<arrow::TimestampType *>(arrowType.get());
    3217           2 :             if (bFoundMin)
    3218             :             {
    3219           1 :                 const int64_t timestamp = sMin.Integer64;
    3220           1 :                 OGRArrowLayer::TimestampToOGR(timestamp, timestampType,
    3221             :                                               poFieldDefn->GetTZFlag(), &sMin);
    3222             :             }
    3223           2 :             if (bFoundMax)
    3224             :             {
    3225           1 :                 const int64_t timestamp = sMax.Integer64;
    3226           1 :                 OGRArrowLayer::TimestampToOGR(timestamp, timestampType,
    3227             :                                               poFieldDefn->GetTZFlag(), &sMax);
    3228             :             }
    3229           2 :             eType = OFTDateTime;
    3230             :         }
    3231             :     }
    3232             : 
    3233         254 :     return bRet;
    3234             : }
    3235             : 
    3236             : /************************************************************************/
    3237             : /*                       GetMinMaxForParquetCol()                       */
    3238             : /************************************************************************/
    3239             : 
    3240        1067 : bool OGRParquetLayer::GetMinMaxForParquetCol(
    3241             :     int iRowGroup,  // -1 for all
    3242             :     int iCol,
    3243             :     const std::shared_ptr<arrow::DataType> &arrowType,  // potentially nullptr
    3244             :     bool bComputeMin, OGRField &sMin, bool &bFoundMin, bool bComputeMax,
    3245             :     OGRField &sMax, bool &bFoundMax, OGRFieldType &eType,
    3246             :     OGRFieldSubType &eSubType, std::string &osMinTmp,
    3247             :     std::string &osMaxTmp) const
    3248             : {
    3249        1067 :     OGR_RawField_SetNull(&sMin);
    3250        1067 :     OGR_RawField_SetNull(&sMax);
    3251        1067 :     eType = OFTReal;
    3252        1067 :     eSubType = OFSTNone;
    3253        1067 :     bFoundMin = false;
    3254        1067 :     bFoundMax = false;
    3255             : 
    3256        2134 :     const auto metadata = GetReader()->parquet_reader()->metadata();
    3257        1067 :     const auto numRowGroups = metadata->num_row_groups();
    3258             : 
    3259        1067 :     if (numRowGroups == 0)
    3260           0 :         return false;
    3261             : 
    3262        2134 :     const auto rowGroup0 = metadata->RowGroup(0);
    3263        1067 :     if (iCol < 0 || iCol >= rowGroup0->num_columns())
    3264             :     {
    3265           0 :         CPLError(CE_Failure, CPLE_AppDefined,
    3266             :                  "GetMinMaxForParquetCol(): invalid iCol=%d", iCol);
    3267           0 :         return false;
    3268             :     }
    3269        2134 :     const auto rowGroup0columnChunk = rowGroup0->ColumnChunk(iCol);
    3270        2134 :     const auto rowGroup0Stats = rowGroup0columnChunk->statistics();
    3271        1067 :     if (!(rowGroup0columnChunk->is_stats_set() && rowGroup0Stats))
    3272             :     {
    3273           0 :         CPLDebug("PARQUET", "Statistics not available for field %s",
    3274           0 :                  rowGroup0columnChunk->path_in_schema()->ToDotString().c_str());
    3275           0 :         return false;
    3276             :     }
    3277             : 
    3278        1067 :     const auto physicalType = rowGroup0Stats->physical_type();
    3279             : 
    3280        1067 :     if (bComputeMin)
    3281             :     {
    3282         651 :         if (physicalType == parquet::Type::BOOLEAN)
    3283             :         {
    3284          54 :             eType = OFTInteger;
    3285          54 :             eSubType = OFSTBoolean;
    3286          54 :             sMin.Integer = GetStats<parquet::BoolStatistics>::min(
    3287             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3288             :         }
    3289         597 :         else if (physicalType == parquet::Type::INT32)
    3290             :         {
    3291          78 :             if (arrowType && arrowType->id() == arrow::Type::UINT32)
    3292             :             {
    3293             :                 // With parquet file version 2.0,
    3294             :                 // statistics of uint32 fields are
    3295             :                 // stored as signed int32 values...
    3296           1 :                 eType = OFTInteger64;
    3297           1 :                 int nVal = GetStats<parquet::Int32Statistics>::min(
    3298             :                     metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3299           1 :                 if (bFoundMin)
    3300             :                 {
    3301           1 :                     sMin.Integer64 = static_cast<uint32_t>(nVal);
    3302             :                 }
    3303             :             }
    3304             :             else
    3305             :             {
    3306          77 :                 eType = OFTInteger;
    3307          77 :                 if (arrowType && arrowType->id() == arrow::Type::INT16)
    3308           1 :                     eSubType = OFSTInt16;
    3309          77 :                 sMin.Integer = GetStats<parquet::Int32Statistics>::min(
    3310             :                     metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3311             :             }
    3312             :         }
    3313         519 :         else if (physicalType == parquet::Type::INT64)
    3314             :         {
    3315          37 :             eType = OFTInteger64;
    3316          37 :             sMin.Integer64 = GetStats<parquet::Int64Statistics>::min(
    3317             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3318             :         }
    3319         482 :         else if (physicalType == parquet::Type::FLOAT)
    3320             :         {
    3321         138 :             eType = OFTReal;
    3322         138 :             eSubType = OFSTFloat32;
    3323         138 :             sMin.Real = GetStats<parquet::FloatStatistics>::min(
    3324             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3325             :         }
    3326         344 :         else if (physicalType == parquet::Type::DOUBLE)
    3327             :         {
    3328         302 :             eType = OFTReal;
    3329         302 :             sMin.Real = GetStats<parquet::DoubleStatistics>::min(
    3330             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3331             :         }
    3332          42 :         else if (arrowType &&
    3333          53 :                  (arrowType->id() == arrow::Type::STRING ||
    3334          95 :                   arrowType->id() == arrow::Type::LARGE_STRING) &&
    3335             :                  physicalType == parquet::Type::BYTE_ARRAY)
    3336             :         {
    3337          78 :             osMinTmp = GetStats<parquet::ByteArrayStatistics>::min(
    3338          39 :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMin);
    3339          39 :             if (bFoundMin)
    3340             :             {
    3341          39 :                 eType = OFTString;
    3342          39 :                 sMin.String = &osMinTmp[0];
    3343             :             }
    3344             :         }
    3345             :     }
    3346             : 
    3347        1067 :     if (bComputeMax)
    3348             :     {
    3349         640 :         if (physicalType == parquet::Type::BOOLEAN)
    3350             :         {
    3351          54 :             eType = OFTInteger;
    3352          54 :             eSubType = OFSTBoolean;
    3353          54 :             sMax.Integer = GetStats<parquet::BoolStatistics>::max(
    3354             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3355             :         }
    3356         586 :         else if (physicalType == parquet::Type::INT32)
    3357             :         {
    3358          78 :             if (arrowType && arrowType->id() == arrow::Type::UINT32)
    3359             :             {
    3360             :                 // With parquet file version 2.0,
    3361             :                 // statistics of uint32 fields are
    3362             :                 // stored as signed int32 values...
    3363           1 :                 eType = OFTInteger64;
    3364           1 :                 int nVal = GetStats<parquet::Int32Statistics>::max(
    3365             :                     metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3366           1 :                 if (bFoundMax)
    3367             :                 {
    3368           1 :                     sMax.Integer64 = static_cast<uint32_t>(nVal);
    3369             :                 }
    3370             :             }
    3371             :             else
    3372             :             {
    3373          77 :                 eType = OFTInteger;
    3374          77 :                 if (arrowType && arrowType->id() == arrow::Type::INT16)
    3375           1 :                     eSubType = OFSTInt16;
    3376          77 :                 sMax.Integer = GetStats<parquet::Int32Statistics>::max(
    3377             :                     metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3378             :             }
    3379             :         }
    3380         508 :         else if (physicalType == parquet::Type::INT64)
    3381             :         {
    3382          37 :             eType = OFTInteger64;
    3383          37 :             sMax.Integer64 = GetStats<parquet::Int64Statistics>::max(
    3384             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3385             :         }
    3386         471 :         else if (physicalType == parquet::Type::FLOAT)
    3387             :         {
    3388         128 :             eType = OFTReal;
    3389         128 :             eSubType = OFSTFloat32;
    3390         128 :             sMax.Real = GetStats<parquet::FloatStatistics>::max(
    3391             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3392             :         }
    3393         343 :         else if (physicalType == parquet::Type::DOUBLE)
    3394             :         {
    3395         301 :             eType = OFTReal;
    3396         301 :             sMax.Real = GetStats<parquet::DoubleStatistics>::max(
    3397             :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3398             :         }
    3399          42 :         else if (arrowType &&
    3400          53 :                  (arrowType->id() == arrow::Type::STRING ||
    3401          95 :                   arrowType->id() == arrow::Type::LARGE_STRING) &&
    3402             :                  physicalType == parquet::Type::BYTE_ARRAY)
    3403             :         {
    3404          78 :             osMaxTmp = GetStats<parquet::ByteArrayStatistics>::max(
    3405          39 :                 metadata, iRowGroup, numRowGroups, iCol, bFoundMax);
    3406          39 :             if (bFoundMax)
    3407             :             {
    3408          39 :                 eType = OFTString;
    3409          39 :                 sMax.String = &osMaxTmp[0];
    3410             :             }
    3411             :         }
    3412             :     }
    3413             : 
    3414        1067 :     return bFoundMin || bFoundMax;
    3415             : }
    3416             : 
    3417             : /************************************************************************/
    3418             : /*                        GeomColsBBOXParquet()                         */
    3419             : /************************************************************************/
    3420             : 
    3421             : /** Return for a given geometry column (iGeom: in [0, GetGeomFieldCount()-1] range),
    3422             :  * the Parquet column number of the corresponding xmin,ymin,xmax,ymax bounding
    3423             :  * box columns, if existing.
    3424             :  */
    3425           1 : bool OGRParquetLayer::GeomColsBBOXParquet(int iGeom, int &iParquetXMin,
    3426             :                                           int &iParquetYMin, int &iParquetXMax,
    3427             :                                           int &iParquetYMax) const
    3428             : {
    3429           1 :     const auto oIter = m_oMapGeomFieldIndexToGeomColBBOXParquet.find(iGeom);
    3430             :     const bool bFound =
    3431           1 :         (oIter != m_oMapGeomFieldIndexToGeomColBBOXParquet.end());
    3432           1 :     if (bFound)
    3433             :     {
    3434           1 :         iParquetXMin = oIter->second.iParquetXMin;
    3435           1 :         iParquetYMin = oIter->second.iParquetYMin;
    3436           1 :         iParquetXMax = oIter->second.iParquetXMax;
    3437           1 :         iParquetYMax = oIter->second.iParquetYMax;
    3438             :     }
    3439           1 :     return bFound;
    3440             : }

Generated by: LCOV version 1.14