laskoviymishka commented on code in PR #1634:
URL: https://github.com/apache/iceberg-go/pull/1634#discussion_r3714517959


##########
table/arrow_utils_test.go:
##########
@@ -2756,3 +2774,244 @@ func 
TestToRequestedSchemaMismatchedExtensionNameNotRewrapped(t *testing.T) {
        // silently rewrapped.
        require.Error(t, err)
 }
+
+// absentCRSWKBType returns the geoarrow.wkb type a reader reconstructs for a
+// column whose stored GeoArrow metadata carries no CRS. Omitted edges mean
+// planar, so the geography cases must state their edge algorithm.
+func absentCRSWKBType(edges geoarrow.EdgeInterpolation) arrow.ExtensionType {
+       return geoarrow.NewWKBType(geoarrow.WKBWithBinaryStorage(),
+               geoarrow.WKBWithMetadata(geoarrow.Metadata{Edges: edges}))
+}
+
+func TestArrowGeoAbsentCRSMetadataToIcebergDefaults(t *testing.T) {
+       tests := []struct {
+               name     string
+               edges    geoarrow.EdgeInterpolation
+               want     iceberg.Type
+               wantType string
+       }{
+               {
+                       name:     "geometry_crs_omitted",
+                       want:     iceberg.GeometryType{},
+                       wantType: "geometry",
+               },
+               {
+                       name:     "geography_crs_omitted",
+                       edges:    geoarrow.EdgeSpherical,
+                       want:     iceberg.GeographyType{},
+                       wantType: "geography",
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       dt := absentCRSWKBType(tt.edges)
+
+                       sc, err := 
table.ArrowSchemaToIceberg(arrow.NewSchema([]arrow.Field{{
+                               Name: "geom", Type: dt, Nullable: true, 
Metadata: fieldIDMeta("1"),
+                       }}, nil), false, nil)
+                       require.NoError(t, err)
+
+                       field, ok := sc.FindFieldByID(1)
+                       require.True(t, ok)
+                       assert.Equal(t, tt.wantType, field.Type.String())
+                       assert.True(t, tt.want.Equals(field.Type), "expected 
%s, got %s", tt.want, field.Type)
+               })
+       }
+}
+
+func TestToRequestedSchemaGeoAbsentCRSProjection(t *testing.T) {
+       values := wkbPointRows()
+
+       tests := []struct {
+               name        string
+               edges       geoarrow.EdgeInterpolation
+               icebergType iceberg.Type
+       }{
+               {
+                       name:        "geometry_crs_omitted",
+                       icebergType: iceberg.GeometryType{},
+               },
+               {
+                       name:        "geography_crs_omitted",
+                       edges:       geoarrow.EdgeSpherical,
+                       icebergType: iceberg.GeographyType{},
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                       defer mem.AssertSize(t, 0)
+
+                       sourceType := absentCRSWKBType(tt.edges)
+                       source := newExtensionArrayOverBinary(t, mem, 
sourceType, values)
+                       defer source.Release()
+
+                       rec := singleColumnRecord(arrow.Field{
+                               Name: "geom", Type: sourceType, Nullable: true, 
Metadata: fieldIDMeta("1"),
+                       }, source)
+                       defer rec.Release()
+
+                       fileSchema, err := 
table.ArrowSchemaToIceberg(rec.Schema(), false, nil)
+                       require.NoError(t, err)
+
+                       requested := iceberg.NewSchema(0, iceberg.NestedField{
+                               ID: 1, Name: "geom", Type: tt.icebergType, 
Required: false,
+                       })
+
+                       ctx := compute.WithAllocator(context.Background(), mem)
+                       out, err := table.ToRequestedSchema(ctx, requested, 
fileSchema, rec, table.SchemaOptions{IncludeFieldIDs: true})
+                       require.NoError(t, err)
+                       defer out.Release()
+
+                       want, err := table.TypeToArrowType(tt.icebergType, 
true, false)
+                       require.NoError(t, err)
+
+                       got, ok := out.Column(0).(array.ExtensionArray)
+                       require.True(t, ok, "expected extension array, got %T", 
out.Column(0))
+                       assert.True(t, arrow.TypeEqual(want, got.DataType()), 
"expected: %s\ngot: %s", want, got.DataType())
+
+                       storage := got.Storage()
+                       require.Equal(t, len(values), storage.Len())
+
+                       for i, v := range values {
+                               if v == nil {
+                                       assert.True(t, storage.IsNull(i), "row 
%d should be null", i)
+
+                                       continue
+                               }
+                               require.False(t, storage.IsNull(i), "row %d 
should not be null", i)
+                               assert.Equal(t, v, binaryValueAt(t, storage, i))
+                       }
+               })
+       }
+}
+
+// roundTripWKBType returns the Arrow type a reader reconstructs for a column
+// written from icebergType, i.e. what a table sees reading its own files.
+func roundTripWKBType(t *testing.T, icebergType iceberg.Type) 
arrow.ExtensionType {
+       t.Helper()
+
+       written, err := table.TypeToArrowType(icebergType, true, false)
+       require.NoError(t, err)
+
+       wkb, ok := written.(*geoarrow.WKBType)
+       require.True(t, ok, "expected geoarrow.wkb, got %T", written)
+
+       dt, err := geoarrow.NewWKBType().Deserialize(wkb.StorageType(), 
wkb.Serialize())
+       require.NoError(t, err)
+
+       return dt
+}
+
+func TestToRequestedSchemaGeoSRID0Projection(t *testing.T) {

Review Comment:
   This covers `srid:0` written by the new code and read straight back, which 
is the easy direction. The case that regresses is the other one: a file with 
absent CRS (what old iceberg-go wrote for a `srid:0` column) projected against 
a `geometry(srid:0)` schema. I'd add a test pinning that path, either asserting 
the promotion now succeeds (if we allow it) or documenting the failure, so the 
back-compat behavior is deliberate rather than incidental.



##########
table/arrow_utils.go:
##########
@@ -2340,10 +2347,6 @@ func icebergCRSToGeoArrowMetadata(crs string, props 
iceberg.Properties) (geoarro
        if strings.HasPrefix(lowerCRS, "srid:") {
                id := crs[len("srid:"):]
 
-               if id == "0" {
-                       return geoarrow.NewMetadata(), nil // srid:0 maps to 
omitted GeoArrow CRS
-               }
-
                raw, _ := json.Marshal(id) //nolint:errcheck // Marshalling a 
string can't fail

Review Comment:
   I think there's a subtle issue with what actually lands in the Parquet CRS 
field here. Dropping the `srid:0` special case means 
`icebergCRSToGeoArrowMetadata("srid:0")` now emits GeoArrow crs `"0"` with 
crs_type srid, and when that serializes out, the Parquet GEOMETRY logical type 
carries the bare string `"0"`.
   
   The Parquet geospatial spec wants the full `"srid:0"` form with the prefix, 
and arrow-rs keys its unknown-CRS sentinel off exactly `"srid:0"`: it won't 
read `"0"` as SRID 0, it'll treat it as an opaque authority code. So we'd be 
writing `srid:0` geometry other engines can't round-trip. I'd store the full 
`"srid:<id>"` string in the GeoArrow CRS field so `ParquetCRS()` emits 
`"srid:0"`. Can we confirm what `ParquetCRS()` produces here and pin it with a 
Parquet-level round-trip?



##########
table/arrow_utils.go:
##########
@@ -2183,9 +2183,16 @@ func isWKT2CRSString(crs string) bool {
        return false
 }
 
+// defaultGeoCRS is the CRS assumed when a Parquet GEOMETRY or GEOGRAPHY 
logical
+// type carries no CRS; it is also the default CRS of the Iceberg geo types.
+const defaultGeoCRS = "OGC:CRS84"

Review Comment:
   iceberg already has an unexported `defaultGeoCRS = "OGC:CRS84"` over in 
types.go, so this is a second copy of the same magic string with no compiler 
link between them. I'd export `iceberg.DefaultGeoCRS` and use it here; the new 
internal-test assertions hard-code `"OGC:CRS84"` too, same drift risk. Minor, 
but it's the kind of thing that silently diverges.



##########
table/arrow_utils.go:
##########
@@ -2183,9 +2183,16 @@ func isWKT2CRSString(crs string) bool {
        return false
 }
 
+// defaultGeoCRS is the CRS assumed when a Parquet GEOMETRY or GEOGRAPHY 
logical
+// type carries no CRS; it is also the default CRS of the Iceberg geo types.
+const defaultGeoCRS = "OGC:CRS84"
+
+// geoArrowCRSToIcebergCRS maps GeoArrow CRS metadata to an Iceberg CRS string.
+// Absent CRS metadata means the default CRS OGC:CRS84, matching the Parquet
+// geospatial spec and Iceberg's default geometry/geography types.
 func geoArrowCRSToIcebergCRS(meta geoarrow.Metadata) (string, error) {
        if len(meta.CRS) == 0 {
-               return "srid:0", nil
+               return defaultGeoCRS, nil

Review Comment:
   I think this changes the read result for files old iceberg-go already wrote. 
Before, a `geometry(srid:0)` column serialized to absent GeoArrow CRS (via the 
`srid:0` to omitted special case this PR also removes), and absent CRS read 
back as `srid:0`, internally consistent. After this change, absent CRS reads as 
plain `geometry`, so projecting one of those files against a `geometry(srid:0)` 
table schema hits PromoteType's default branch and fails with `cannot promote 
geometry to geometry(srid:0)`.
   
   Blast radius is bounded, since that write path only shipped a few weeks ago, 
but it's a silent read regression for anyone who wrote `srid:0` geometry in 
that window. I'd allow `geometry(srid:0)` → `geometry` (and `geography(srid:0, 
spherical)` → `geography`) as permitted promotions, since old iceberg-go used 
`srid:0` as the stand-in for absent CRS anyway; at minimum I'd document the 
migration. wdyt?



##########
table/arrow_utils_internal_test.go:
##########
@@ -495,11 +496,16 @@ func TestIcebergCRSToGeoArrowMetadata(t *testing.T) {
                }
        })
 
-       t.Run("srid:0 maps to an omitted CRS", func(t *testing.T) {
+       // An omitted CRS means OGC:CRS84, so the unknown CRS srid:0 must be 
explicit.
+       t.Run("srid:0 round trips as an explicit srid CRS", func(t *testing.T) {
                meta, err := icebergCRSToGeoArrowMetadata("srid:0", nil)
                require.NoError(t, err)
-               assert.Empty(t, meta.CRS)
-               assert.Empty(t, meta.CRSType)
+               assert.JSONEq(t, `"0"`, string(meta.CRS))
+               assert.Equal(t, geoarrow.CRSTypeSRID, meta.CRSType)
+
+               crs, err := geoArrowCRSToIcebergCRS(meta)
+               require.NoError(t, err)
+               assert.Equal(t, "srid:0", crs)

Review Comment:
   This round-trips through the GeoArrow metadata but stops there: it never 
serializes to a Parquet CRS string and reads it back, which is exactly where 
the `"0"` vs `"srid:0"` question bites. It'd still pass even if what we write 
to Parquet is `"0"` rather than the spec's `"srid:0"`. A round-trip that goes 
iceberg type → Parquet CRS → iceberg type would pin the wire format we actually 
care about.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to