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


##########
table/arrow_utils.go:
##########
@@ -1871,6 +1871,46 @@ func checkCRSJSON(rawCrs json.RawMessage) bool {
        return len(b) > 0 && b[0] == '{'
 }
 
+// errWKT2CRSNotSupported is returned for WKT2:2019 CRS definitions, which this
+// package does not support yet.
+var errWKT2CRSNotSupported = errors.New("CRS type wkt2:2019 not supported")
+
+// errPROJJSONStringCRSNotSupported is returned for projjson-typed string CRS
+// values, which this package does not support yet.
+var errPROJJSONStringCRSNotSupported = errors.New("CRS type projjson not 
supported for string CRS")
+
+// isWKT2CRSString reports whether crs looks like a WKT2 (ISO 19162) CRS
+// definition. It is a best-effort heuristic used only to surface the clearer
+// errWKT2CRSNotSupported message. The list covers the WKT2:2019 CRS keywords
+// and their long forms.
+func isWKT2CRSString(crs string) bool {
+       upper := strings.ToUpper(strings.TrimSpace(crs))
+
+       for _, prefix := range []string{
+               "BOUNDCRS[",
+               "COMPOUNDCRS[",
+               "DERIVEDPROJCRS[",
+               "ENGCRS[",
+               "ENGINEERINGCRS[",
+               "GEODCRS[",
+               "GEODETICCRS[",
+               "GEOGCRS[",

Review Comment:
   The keyword set is missing a few valid WKT2:2019 top-level forms — 
`COORDINATEMETADATA[`, `COORDINATEOPERATION[`, and the abstract `CRS[` wrapper 
— so a string wrapped in those passes the heuristic and gets stored as an 
opaque CRS instead of hitting the clearer error. I'd add them.
   
   While we're here: this is a best-effort prefix match, so it'll also fire on 
a non-WKT2 authority string that happens to start with one of these tokens — 
worth a one-line comment that it's intentionally heuristic and only used to 
pick the friendlier error.



##########
table/arrow_utils.go:
##########
@@ -1883,23 +1923,32 @@ func geoArrowCRSToIcebergCRS(meta geoarrow.Metadata) 
(string, error) {
                if err := json.Unmarshal(meta.CRS, &crs); err != nil {
                        return "", fmt.Errorf("invalid geoarrow CRS metadata: 
%w", err)
                }
-
-               if strings.EqualFold(crs, "OGC:CRS84") || 
strings.EqualFold(crs, "EPSG:4326") {
-                       return "OGC:CRS84", nil
+               if crs == "" {
+                       return "", errors.New("unsupported CRS: empty string 
CRS")
                }
 
                switch meta.CRSType {
-               case geoarrow.CRSTypeSRID:
-                       return "srid:" + crs, nil
+               case geoarrow.CRSTypePROJJSON:

Review Comment:
   I think there's a subtle inconsistency here. The SRID arm was deliberately 
moved below the canonical check (with the comment a few lines down) so that 
`EPSG:4326 + crs_type=srid` still canonicalizes to `OGC:CRS84` rather than 
becoming `srid:EPSG:4326`. But projjson and wkt2:2019 fire *before* the 
canonical check, so `{"crs":"EPSG:4326","crs_type":"projjson"}` and 
`{"crs":"EPSG:4326","crs_type":"wkt2:2019"}` both error — that's exactly the 
case the deleted `geometry_epsg_4326_incorrect_type` test used to cover.
   
   A canonical authority code is unambiguous regardless of a bogus `crs_type`, 
so I'd apply the same rule you already applied to SRID: run the 
`OGC:CRS84`/`EPSG:4326` check first, then let projjson/wkt2:2019 reject only 
non-canonical strings. That makes all four arms consistent. wdyt?



##########
table/arrow_utils.go:
##########
@@ -1883,23 +1923,32 @@ func geoArrowCRSToIcebergCRS(meta geoarrow.Metadata) 
(string, error) {
                if err := json.Unmarshal(meta.CRS, &crs); err != nil {
                        return "", fmt.Errorf("invalid geoarrow CRS metadata: 
%w", err)
                }
-
-               if strings.EqualFold(crs, "OGC:CRS84") || 
strings.EqualFold(crs, "EPSG:4326") {
-                       return "OGC:CRS84", nil
+               if crs == "" {
+                       return "", errors.New("unsupported CRS: empty string 
CRS")
                }
 
                switch meta.CRSType {
-               case geoarrow.CRSTypeSRID:
-                       return "srid:" + crs, nil
+               case geoarrow.CRSTypePROJJSON:
+                       return "", errPROJJSONStringCRSNotSupported
                case geoarrow.CRSTypeWKT22019:
-                       return "", errors.New("CRS type wkt2:2019 not 
supported")
-               default:
-                       if len(crs) <= 32 {
-                               return crs, nil
-                       }
+                       return "", errWKT2CRSNotSupported
+               }
 
-                       return "", errors.New("crs length too long")
+               if strings.EqualFold(crs, "OGC:CRS84") || 
strings.EqualFold(crs, "EPSG:4326") {

Review Comment:
   This is the move I'd make to fix the ordering above: lift this canonical 
block to the very top of the string case, right after the empty-string guard 
and before `switch meta.CRSType`. Once it short-circuits first, the projjson 
and wkt2:2019 arms can stay as-is and the SRID special-casing comment below 
becomes unnecessary, since canonical will already have returned.



##########
table/arrow_utils_internal_test.go:
##########
@@ -491,3 +492,128 @@ func TestIcebergCRSToGeoArrowMetadata(t *testing.T) {
                assert.Empty(t, meta.CRSType)
        })
 }
+
+func TestGeoArrowCRSToIcebergCRS(t *testing.T) {
+       stringCRS := func(s string) json.RawMessage {

Review Comment:
   `stringCRS` closes over the outer `t`, so a `require.NoError` failure inside 
a subtest gets reported against the parent. `json.Marshal` of a string can't 
fail, so I'd just drop the error check (or take a `testing.TB` and call 
`tb.Helper()` if you want to keep it defensive).



##########
table/arrow_utils_internal_test.go:
##########
@@ -491,3 +492,128 @@ func TestIcebergCRSToGeoArrowMetadata(t *testing.T) {
                assert.Empty(t, meta.CRSType)
        })
 }
+
+func TestGeoArrowCRSToIcebergCRS(t *testing.T) {
+       stringCRS := func(s string) json.RawMessage {
+               raw, err := json.Marshal(s)
+               require.NoError(t, err)
+
+               return raw
+       }
+
+       t.Run("accepts long authority code string", func(t *testing.T) {
+               const crs = "EPSGAuthorityLongName:CODE-12345678901234567890"
+
+               got, err := geoArrowCRSToIcebergCRS(geoarrow.Metadata{CRS: 
stringCRS(crs)})
+               require.NoError(t, err)
+               assert.Equal(t, crs, got)
+       })
+
+       t.Run("accepts arbitrary string CRS", func(t *testing.T) {
+               got, err := geoArrowCRSToIcebergCRS(geoarrow.Metadata{CRS: 
stringCRS("custom")})
+               require.NoError(t, err)
+               assert.Equal(t, "custom", got)
+       })
+
+       t.Run("canonical string CRS wins over srid type annotation", func(t 
*testing.T) {
+               for _, crs := range []string{"EPSG:4326", "OGC:CRS84"} {
+                       t.Run(crs, func(t *testing.T) {
+                               got, err := 
geoArrowCRSToIcebergCRS(geoarrow.Metadata{
+                                       CRS:     stringCRS(crs),
+                                       CRSType: geoarrow.CRSTypeSRID,
+                               })
+                               require.NoError(t, err)
+                               assert.Equal(t, "OGC:CRS84", got)
+                       })
+               }
+       })
+
+       t.Run("numeric srid type annotation maps to srid", func(t *testing.T) {
+               got, err := geoArrowCRSToIcebergCRS(geoarrow.Metadata{
+                       CRS:     stringCRS("3857"),
+                       CRSType: geoarrow.CRSTypeSRID,
+               })
+               require.NoError(t, err)
+               assert.Equal(t, "srid:3857", got)
+       })
+
+       t.Run("rejects empty string CRS", func(t *testing.T) {
+               _, err := geoArrowCRSToIcebergCRS(geoarrow.Metadata{CRS: 
stringCRS("")})
+               require.Error(t, err)
+               assert.ErrorContains(t, err, "unsupported CRS: empty string 
CRS")
+       })
+
+       t.Run("rejects projjson string CRS", func(t *testing.T) {
+               _, err := geoArrowCRSToIcebergCRS(geoarrow.Metadata{
+                       CRS:     stringCRS("EPSG:4326"),
+                       CRSType: geoarrow.CRSTypePROJJSON,
+               })
+               require.Error(t, err)
+               assert.ErrorContains(t, err, "CRS type projjson not supported 
for string CRS")

Review Comment:
   These two are package-level sentinels (`errPROJJSONStringCRSNotSupported` 
here and `errWKT2CRSNotSupported` just below), and this is the internal test, 
so it can reach them — I'd assert with `assert.ErrorIs(t, err, 
errPROJJSONStringCRSNotSupported)` rather than matching the message text. Keeps 
the test from breaking on wording changes and actually pins the sentinel. (The 
external `arrow_utils_test.go` can't see the unexported vars, so 
`ErrorContains` is correct there.)



##########
table/arrow_utils.go:
##########
@@ -1883,23 +1923,32 @@ func geoArrowCRSToIcebergCRS(meta geoarrow.Metadata) 
(string, error) {
                if err := json.Unmarshal(meta.CRS, &crs); err != nil {
                        return "", fmt.Errorf("invalid geoarrow CRS metadata: 
%w", err)
                }
-
-               if strings.EqualFold(crs, "OGC:CRS84") || 
strings.EqualFold(crs, "EPSG:4326") {
-                       return "OGC:CRS84", nil
+               if crs == "" {
+                       return "", errors.New("unsupported CRS: empty string 
CRS")
                }
 
                switch meta.CRSType {
-               case geoarrow.CRSTypeSRID:
-                       return "srid:" + crs, nil
+               case geoarrow.CRSTypePROJJSON:
+                       return "", errPROJJSONStringCRSNotSupported
                case geoarrow.CRSTypeWKT22019:
-                       return "", errors.New("CRS type wkt2:2019 not 
supported")
-               default:
-                       if len(crs) <= 32 {
-                               return crs, nil
-                       }
+                       return "", errWKT2CRSNotSupported
+               }
 
-                       return "", errors.New("crs length too long")
+               if strings.EqualFold(crs, "OGC:CRS84") || 
strings.EqualFold(crs, "EPSG:4326") {
+                       return "OGC:CRS84", nil
+               }
+               if isWKT2CRSString(crs) {
+                       return "", errWKT2CRSNotSupported
                }
+               // SRID is resolved after the canonical and WKT2 checks so a
+               // contradictory crs_type=srid on a value like "EPSG:4326" still
+               // canonicalizes rather than producing "srid:EPSG:4326". Real 
numeric
+               // SRIDs match none of the checks above and fall through to 
here.
+               if meta.CRSType == geoarrow.CRSTypeSRID {
+                       return "srid:" + crs, nil
+               }
+
+               return crs, nil

Review Comment:
   This final `return crs, nil` is the catch-all passthrough for AuthorityCode 
and unset types, but that policy is implicit. One line — something like 
"unrecognized type or bare string: accept as opaque CRS per spec (any unique 
identifier is valid)" — would save the next reader the trace-through.



##########
table/arrow_utils_test.go:
##########
@@ -608,11 +615,6 @@ func TestIcebergGeoTypesToArrowSchema(t *testing.T) {
                        ice:              defaultGeometry,
                        geoarrowMetaJSON: `{"crs":"epsg:4326"}`,
                },
-               {
-                       name:             "geometry_epsg_4326_incorrect_type",

Review Comment:
   Once the canonical-first reordering lands, this case should come back with 
its original canonicalizing assertion (`OGC:CRS84`) — it's the regression guard 
for the exact ordering bug. I'd also add the sibling 
`{"crs":"EPSG:4326","crs_type":"wkt2:2019"}` case, since that path is currently 
untested and is where the expected behavior is easiest to get wrong.



##########
table/arrow_utils.go:
##########
@@ -1883,23 +1923,32 @@ func geoArrowCRSToIcebergCRS(meta geoarrow.Metadata) 
(string, error) {
                if err := json.Unmarshal(meta.CRS, &crs); err != nil {
                        return "", fmt.Errorf("invalid geoarrow CRS metadata: 
%w", err)
                }
-
-               if strings.EqualFold(crs, "OGC:CRS84") || 
strings.EqualFold(crs, "EPSG:4326") {
-                       return "OGC:CRS84", nil
+               if crs == "" {
+                       return "", errors.New("unsupported CRS: empty string 
CRS")
                }
 
                switch meta.CRSType {
-               case geoarrow.CRSTypeSRID:
-                       return "srid:" + crs, nil
+               case geoarrow.CRSTypePROJJSON:
+                       return "", errPROJJSONStringCRSNotSupported
                case geoarrow.CRSTypeWKT22019:
-                       return "", errors.New("CRS type wkt2:2019 not 
supported")
-               default:
-                       if len(crs) <= 32 {
-                               return crs, nil
-                       }
+                       return "", errWKT2CRSNotSupported
+               }

Review Comment:
   This switch handles two of the four `CRSType` constants with no `default`, 
which the exhaustive linter will flag, and means a future fifth constant 
silently falls through to `return crs, nil`. I'd add an explicit `default:` 
with a short comment that SRID/AuthorityCode/unset are intentionally handled by 
the checks below.



-- 
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