This is an automated email from the ASF dual-hosted git repository.

wgtmac pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git


The following commit(s) were added to refs/heads/main by this push:
     new 62c5c2f8 fix(parquet): validate geospatial projection parameters 
(#1050)
62c5c2f8 is described below

commit 62c5c2f81b1d31622e343890b4cd0ba7d2a9eca9
Author: Manu Zhang <[email protected]>
AuthorDate: Thu Oct 22 20:46:12 2026 +0800

    fix(parquet): validate geospatial projection parameters (#1050)
    
    Reject incompatible geometry/geography CRS and geography edge algorithms 
during projection. Resolve omitted Parquet CRS to OGC:CRS84 and include the 
field path in mismatch errors.
    
    Extract common CRS normalization, parameter comparison, and mismatch 
reporting into a helper used by geometry and geography projection.
    
    Co-authored-by: Codex <[email protected]>
---
 src/iceberg/parquet/parquet_schema_util.cc |  26 +++++-
 src/iceberg/test/parquet_schema_test.cc    | 122 +++++++++++++++++++++++++++--
 2 files changed, 138 insertions(+), 10 deletions(-)

diff --git a/src/iceberg/parquet/parquet_schema_util.cc 
b/src/iceberg/parquet/parquet_schema_util.cc
index 6a15659e..2a230fe4 100644
--- a/src/iceberg/parquet/parquet_schema_util.cc
+++ b/src/iceberg/parquet/parquet_schema_util.cc
@@ -293,6 +293,20 @@ void SelectAnchorColumnIfEmpty(
   }
 }
 
+template <typename IcebergType, typename ParquetType>
+Status ValidateGeospatialParameters(const IcebergType& expected,
+                                    const ParquetType& actual,
+                                    const ::parquet::ColumnDescriptor& descr,
+                                    bool algorithm_matches = true) {
+  // Parquet represents an omitted CRS as an empty string.
+  const auto crs = actual.crs().empty() ? IcebergType::kDefaultCrs : 
actual.crs();
+  if (!StringUtils::EqualsIgnoreCase(expected.crs(), crs) || 
!algorithm_matches) {
+    return InvalidSchema("Cannot read Parquet type {} as Iceberg type {} for 
field {}",
+                         actual.ToString(), expected, 
descr.path()->ToDotString());
+  }
+  return {};
+}
+
 Status ValidateGeospatialParquetType(const Type& expected_type,
                                      const ::parquet::arrow::SchemaField& 
parquet_field,
                                      const ::parquet::ColumnDescriptor& descr) 
{
@@ -311,12 +325,20 @@ Status ValidateGeospatialParquetType(const Type& 
expected_type,
     if (!descr.logical_type()->is_geometry()) {
       return InvalidSchema("Iceberg geometry requires Parquet Geometry logical 
type");
     }
-    return {};
+    const auto& expected = internal::checked_cast<const 
GeometryType&>(expected_type);
+    const auto& actual = internal::checked_cast<const 
::parquet::GeometryLogicalType&>(
+        *descr.logical_type());
+    return ValidateGeospatialParameters(expected, actual, descr);
   }
   if (!descr.logical_type()->is_geography()) {
     return InvalidSchema("Iceberg geography requires Parquet Geography logical 
type");
   }
-  return {};
+  const auto& expected = internal::checked_cast<const 
GeographyType&>(expected_type);
+  const auto& actual = internal::checked_cast<const 
::parquet::GeographyLogicalType&>(
+      *descr.logical_type());
+  ICEBERG_ASSIGN_OR_RAISE(auto algorithm, 
ToParquetAlgorithm(expected.algorithm()));
+  return ValidateGeospatialParameters(expected, actual, descr,
+                                      algorithm == actual.algorithm());
 }
 
 }  // namespace
diff --git a/src/iceberg/test/parquet_schema_test.cc 
b/src/iceberg/test/parquet_schema_test.cc
index 4cf36da1..c9f50369 100644
--- a/src/iceberg/test/parquet_schema_test.cc
+++ b/src/iceberg/test/parquet_schema_test.cc
@@ -567,27 +567,133 @@ TEST(ParquetGeospatialSchemaTest, 
ConvertsGeospatialTypes) {
   }
 }
 
-TEST(ParquetGeospatialSchemaTest, ProjectsTypesWithDifferentParameters) {
+TEST(ParquetGeospatialSchemaTest, ProjectsTypesWithMatchingParameters) {
   Schema expected_schema({
       SchemaField::MakeOptional(/*field_id=*/1, "geom", iceberg::geometry()),
       SchemaField::MakeOptional(
           /*field_id=*/2, "geog",
           iceberg::geography("epsg:4326", EdgeAlgorithm::kKarney)),
+      SchemaField::MakeOptional(/*field_id=*/3, "geog_default", 
iceberg::geography()),
   });
   auto parquet_schema = MakeGroupNode(
       "iceberg_schema",
-      {MakeBinaryNode("geom", ::parquet::LogicalType::Geometry("EPSG:3857"),
+      {MakeBinaryNode("geom", ::parquet::LogicalType::Geometry(),
                       /*field_id=*/1),
-       MakeBinaryNode("geog",
-                      ::parquet::LogicalType::Geography(
-                          "OGC:CRS84",
-                          
::parquet::LogicalType::EdgeInterpolationAlgorithm::SPHERICAL),
-                      /*field_id=*/2)});
+       MakeBinaryNode(
+           "geog",
+           ::parquet::LogicalType::Geography(
+               "EPSG:4326", 
::parquet::LogicalType::EdgeInterpolationAlgorithm::KARNEY),
+           /*field_id=*/2),
+       MakeBinaryNode("geog_default", ::parquet::LogicalType::Geography(),
+                      /*field_id=*/3)});
   auto manifest = MakeSchemaManifest(parquet_schema);
   ICEBERG_UNWRAP_OR_FAIL(auto projection, Project(expected_schema, manifest));
-  ASSERT_EQ(projection.fields.size(), 2);
+  ASSERT_EQ(projection.fields.size(), 3);
   ASSERT_PROJECTED_FIELD(projection.fields[0], 0);
   ASSERT_PROJECTED_FIELD(projection.fields[1], 1);
+  ASSERT_PROJECTED_FIELD(projection.fields[2], 2);
+}
+
+TEST(ParquetGeospatialSchemaTest, ProjectsCaseInsensitiveCrsAndAllAlgorithms) {
+  using ParquetAlgorithm = ::parquet::LogicalType::EdgeInterpolationAlgorithm;
+  const std::array<std::pair<EdgeAlgorithm, ParquetAlgorithm>, 5> algorithms = 
{{
+      {EdgeAlgorithm::kSpherical, ParquetAlgorithm::SPHERICAL},
+      {EdgeAlgorithm::kVincenty, ParquetAlgorithm::VINCENTY},
+      {EdgeAlgorithm::kThomas, ParquetAlgorithm::THOMAS},
+      {EdgeAlgorithm::kAndoyer, ParquetAlgorithm::ANDOYER},
+      {EdgeAlgorithm::kKarney, ParquetAlgorithm::KARNEY},
+  }};
+  for (const auto& [iceberg_algorithm, parquet_algorithm] : algorithms) {
+    Schema schema({
+        SchemaField::MakeOptional(1, "geom", iceberg::geometry("epsg:3857")),
+        SchemaField::MakeOptional(2, "geog",
+                                  iceberg::geography("ogc:crs84", 
iceberg_algorithm)),
+    });
+    auto parquet_schema = MakeGroupNode(
+        "iceberg_schema",
+        {MakeBinaryNode("geom", ::parquet::LogicalType::Geometry("EPSG:3857"), 
1),
+         MakeBinaryNode("geog",
+                        ::parquet::LogicalType::Geography("OGC:CRS84", 
parquet_algorithm),
+                        2)});
+    auto manifest = MakeSchemaManifest(parquet_schema);
+    ICEBERG_UNWRAP_OR_FAIL(auto projection, Project(schema, manifest));
+    ASSERT_EQ(projection.fields.size(), 2);
+    ASSERT_PROJECTED_FIELD(projection.fields[0], 0);
+    ASSERT_PROJECTED_FIELD(projection.fields[1], 1);
+  }
+}
+
+TEST(ParquetGeospatialSchemaTest, RejectsMismatchedParameters) {
+  using ParquetAlgorithm = ::parquet::LogicalType::EdgeInterpolationAlgorithm;
+  const std::vector<
+      std::pair<std::shared_ptr<Type>, std::shared_ptr<const 
::parquet::LogicalType>>>
+      types = {
+          {iceberg::geometry(), ::parquet::LogicalType::Geometry("EPSG:3857")},
+          {iceberg::geography(), 
::parquet::LogicalType::Geography("EPSG:4326")},
+          {iceberg::geography(),
+           ::parquet::LogicalType::Geography("OGC:CRS84", 
ParquetAlgorithm::KARNEY)},
+          {iceberg::geography("OGC:CRS84", EdgeAlgorithm::kKarney),
+           ::parquet::LogicalType::Geography()},
+      };
+  for (const auto& [expected, actual] : types) {
+    SCOPED_TRACE(expected->ToString() + " from " + actual->ToString());
+    Schema schema({SchemaField::MakeOptional(1, "location", expected)});
+    auto parquet_schema = MakeGroupNode(
+        "iceberg_schema", {MakeBinaryNode("location", actual, 
/*field_id=*/1)});
+    auto manifest = MakeSchemaManifest(parquet_schema);
+    auto result = Project(schema, manifest);
+    ASSERT_THAT(result, IsError(ErrorKind::kInvalidSchema));
+    ASSERT_THAT(result, HasErrorMessage("Cannot read Parquet type"));
+    ASSERT_THAT(result, HasErrorMessage(expected->ToString()));
+    ASSERT_THAT(result, HasErrorMessage(actual->ToString()));
+    ASSERT_THAT(result, HasErrorMessage("for field location"));
+  }
+}
+
+TEST(ParquetGeospatialSchemaTest, ValidatesNestedParameters) {
+  for (bool matching : {true, false}) {
+    SCOPED_TRACE(matching);
+    const auto actual =
+        ::parquet::LogicalType::Geometry(matching ? "ogc:crs84" : "EPSG:3857");
+    const std::vector<SchemaField> fields = {
+        SchemaField::MakeOptional(
+            1, "struct_field",
+            std::make_shared<StructType>(std::vector<SchemaField>{
+                SchemaField::MakeOptional(3, "geom", iceberg::geometry())})),
+        SchemaField::MakeOptional(4, "list_field",
+                                  
std::make_shared<ListType>(SchemaField::MakeOptional(
+                                      5, "element", iceberg::geometry()))),
+        SchemaField::MakeOptional(
+            6, "map_field",
+            std::make_shared<MapType>(
+                SchemaField::MakeRequired(7, "key", iceberg::string()),
+                SchemaField::MakeOptional(8, "value", iceberg::geometry()))),
+    };
+    const std::vector<::parquet::schema::NodePtr> nodes = {
+        MakeGroupNode("struct_field",
+                      {MakeBinaryNode("unselected",
+                                      
::parquet::LogicalType::Geometry("EPSG:3857"), 2),
+                       MakeBinaryNode("geom", actual, 3)},
+                      1),
+        MakeListNode("list_field", MakeBinaryNode("element", actual, 5), 4),
+        MakeMapNode("map_field", MakeStringNode("key", 7, /*optional=*/false),
+                    MakeBinaryNode("value", actual, 8), 6),
+    };
+    const std::array<std::string_view, 3> paths = {
+        "struct_field.geom", "list_field.element.element", 
"map_field.key_value.value"};
+    for (size_t i = 0; i < fields.size(); ++i) {
+      SCOPED_TRACE(paths[i]);
+      Schema schema({fields[i]});
+      auto manifest = MakeSchemaManifest(MakeGroupNode("iceberg_schema", 
{nodes[i]}));
+      auto result = Project(schema, manifest);
+      if (matching) {
+        ASSERT_THAT(result, IsOk());
+      } else {
+        ASSERT_THAT(result, IsError(ErrorKind::kInvalidSchema));
+        ASSERT_THAT(result, HasErrorMessage(std::string(paths[i])));
+      }
+    }
+  }
 }
 
 TEST(ParquetGeospatialSchemaTest, RejectsMissingLogicalType) {

Reply via email to