-
Notifications
You must be signed in to change notification settings - Fork 221
feat(table): WKB encoding + GeoArrow conversion for geometry/geography #1138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
bd6338e
d164e92
f9de98a
41e9041
7894fb1
dc133ae
9a19c16
d55225c
37cbdb8
fb6aeff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -18,12 +18,15 @@ | |||||||
| package table | ||||||||
|
|
||||||||
| import ( | ||||||||
| "bytes" | ||||||||
| "context" | ||||||||
| "encoding/base64" | ||||||||
| "encoding/json" | ||||||||
| "errors" | ||||||||
| "fmt" | ||||||||
| "iter" | ||||||||
| "log/slog" | ||||||||
| "regexp" | ||||||||
| "slices" | ||||||||
| "strconv" | ||||||||
| "strings" | ||||||||
|
|
@@ -375,6 +378,17 @@ func (c convertToIceberg) Primitive(dt arrow.DataType) (result iceberg.NestedFie | |||||||
| result.Type = iceberg.PrimitiveTypes.UUID | ||||||||
| case "parquet.variant": | ||||||||
| result.Type = iceberg.VariantType{} | ||||||||
| case "geoarrow.wkb": | ||||||||
| wkb, ok := dt.(*geoarrow.WKBType) | ||||||||
| if !ok { | ||||||||
| panic(fmt.Errorf("%w: unsupported arrow type for conversion - %s", iceberg.ErrInvalidSchema, dt)) | ||||||||
| } | ||||||||
|
|
||||||||
| iceType, err := geoArrowMetadataToIcebergType(wkb.Metadata()) | ||||||||
| if err != nil { | ||||||||
| panic(fmt.Errorf("%w: converting geoarrow metadata: %w", iceberg.ErrInvalidSchema, err)) | ||||||||
| } | ||||||||
| result.Type = iceType | ||||||||
| default: | ||||||||
| panic(fmt.Errorf("%w: unsupported arrow type for conversion - %s", iceberg.ErrInvalidSchema, dt)) | ||||||||
| } | ||||||||
|
|
@@ -658,20 +672,26 @@ func (c convertToArrow) VisitVariant() arrow.Field { | |||||||
| return arrow.Field{Type: extensions.NewDefaultVariantType()} | ||||||||
| } | ||||||||
|
|
||||||||
| func (c convertToArrow) VisitGeometry(iceberg.GeometryType) arrow.Field { | ||||||||
| func (c convertToArrow) VisitGeometry(g iceberg.GeometryType) arrow.Field { | ||||||||
| meta := icebergCRSToGeoArrowMetadata(g.CRS()) | ||||||||
| if c.useLargeTypes { | ||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithLargeBinaryStorage())} | ||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithLargeBinaryStorage(), geoarrow.WKBWithMetadata(meta))} | ||||||||
| } | ||||||||
|
|
||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithBinaryStorage())} | ||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithBinaryStorage(), geoarrow.WKBWithMetadata(meta))} | ||||||||
| } | ||||||||
|
|
||||||||
| func (c convertToArrow) VisitGeography(iceberg.GeographyType) arrow.Field { | ||||||||
| func (c convertToArrow) VisitGeography(g iceberg.GeographyType) arrow.Field { | ||||||||
| meta := icebergCRSToGeoArrowMetadata(g.CRS()) | ||||||||
| // Always add an edge to differentiate between Geography and Geometry arrow fields. | ||||||||
| // Note that the edge convention is a best-effort hint and planar geography from other clients won't round-trip through Arrow alone. | ||||||||
|
Comment on lines
+686
to
+687
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this comment is misleading (planar geography is not a thing, and this is not a hint, it affects correctness).
Suggested change
|
||||||||
| meta.Edges = geoarrow.EdgeInterpolation(g.Algorithm()) | ||||||||
|
happydave1 marked this conversation as resolved.
|
||||||||
|
|
||||||||
| if c.useLargeTypes { | ||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithLargeBinaryStorage())} | ||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithLargeBinaryStorage(), geoarrow.WKBWithMetadata(meta))} | ||||||||
| } | ||||||||
|
|
||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithBinaryStorage())} | ||||||||
| return arrow.Field{Type: geoarrow.NewWKBType(geoarrow.WKBWithBinaryStorage(), geoarrow.WKBWithMetadata(meta))} | ||||||||
| } | ||||||||
|
|
||||||||
| var _ iceberg.SchemaVisitorPerPrimitiveType[arrow.Field] = convertToArrow{} | ||||||||
|
|
@@ -1826,3 +1846,150 @@ func positionDeleteRecordsToDataFilesDV(ctx context.Context, rootLocation string | |||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| func checkCRSString(rawCrs json.RawMessage) bool { | ||||||||
| b := bytes.TrimSpace(rawCrs) | ||||||||
|
|
||||||||
| return len(b) > 0 && b[0] == '"' | ||||||||
| } | ||||||||
|
|
||||||||
| func checkCRSJSON(rawCrs json.RawMessage) bool { | ||||||||
| b := bytes.TrimSpace(rawCrs) | ||||||||
|
|
||||||||
| return len(b) > 0 && b[0] == '{' | ||||||||
| } | ||||||||
|
|
||||||||
| func geoArrowCRSToIcebergCRS(meta geoarrow.Metadata) (string, error) { | ||||||||
| if len(meta.CRS) == 0 { | ||||||||
| return "srid:0", nil | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bare Separately, an empty
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is the correct behaviour: the GeoArrow default does not equal the Iceberg default. PyIceberg is probably wrong here.
This should be fixed...the CRSType can actually just be ignored for the purposes of this function (it's purely a hint) |
||||||||
| } | ||||||||
|
|
||||||||
| switch { | ||||||||
| case checkCRSString(meta.CRS): | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A small cleanup cluster in this function: The On the write path (line 1984), And in the JSON-object code parsing (line 1923), |
||||||||
| var crs string | ||||||||
|
|
||||||||
| 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 | ||||||||
| } | ||||||||
|
|
||||||||
| switch meta.CRSType { | ||||||||
| case geoarrow.CRSTypeSRID: | ||||||||
| return "srid:" + crs, nil | ||||||||
| case geoarrow.CRSTypeWKT22019: | ||||||||
| return "", errors.New("CRS type wkt2:2019 not supported") | ||||||||
| default: | ||||||||
| if len(crs) <= 32 { | ||||||||
| return crs, nil | ||||||||
| } | ||||||||
|
|
||||||||
| return "", errors.New("crs length too long") | ||||||||
| } | ||||||||
| case checkCRSJSON(meta.CRS): | ||||||||
| var crs map[string]json.RawMessage | ||||||||
|
|
||||||||
| if err := json.Unmarshal(meta.CRS, &crs); err != nil { | ||||||||
| return "", fmt.Errorf("invalid geoarrow CRS metadata: %w", err) | ||||||||
| } | ||||||||
|
|
||||||||
| idRaw, ok := crs["id"] | ||||||||
| if !ok || len(idRaw) == 0 { | ||||||||
| return "", errors.New("unsupported CRS") | ||||||||
| } | ||||||||
|
|
||||||||
| var id map[string]json.RawMessage | ||||||||
| if err := json.Unmarshal(idRaw, &id); err != nil { | ||||||||
| return "", errors.New("unsupported CRS") | ||||||||
| } | ||||||||
|
|
||||||||
| var authority string | ||||||||
| if err := json.Unmarshal(id["authority"], &authority); err != nil || authority == "" { | ||||||||
| return "", errors.New("unsupported CRS") | ||||||||
| } | ||||||||
|
|
||||||||
| codeRaw := id["code"] | ||||||||
| if len(codeRaw) == 0 { | ||||||||
| return "", errors.New("unsupported CRS") | ||||||||
| } | ||||||||
|
|
||||||||
| var code string | ||||||||
| if err := json.Unmarshal(codeRaw, &code); err != nil { | ||||||||
| var codeNum json.Number | ||||||||
| if err := json.Unmarshal(codeRaw, &codeNum); err != nil { | ||||||||
| return "", errors.New("unsupported CRS") | ||||||||
| } | ||||||||
| code = codeNum.String() | ||||||||
| } | ||||||||
| if code == "" { | ||||||||
| return "", errors.New("unsupported CRS") | ||||||||
| } | ||||||||
|
|
||||||||
| authorityCode := authority + ":" + code | ||||||||
| if strings.EqualFold(authorityCode, "OGC:CRS84") || strings.EqualFold(authorityCode, "EPSG:4326") { | ||||||||
| return "OGC:CRS84", nil | ||||||||
| } | ||||||||
|
|
||||||||
| return authorityCode, nil | ||||||||
| default: | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'd add an explicit
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
|
||||||||
| return "", errors.New("unsupported CRS: CRS must either be omitted, a string, or a JSON object") | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| func geoArrowMetadataToIcebergType(meta geoarrow.Metadata) (iceberg.Type, error) { | ||||||||
| crs, err := geoArrowCRSToIcebergCRS(meta) | ||||||||
| if err != nil { | ||||||||
| return nil, err | ||||||||
| } | ||||||||
|
|
||||||||
| switch meta.Edges { | ||||||||
| case geoarrow.EdgePlanar: | ||||||||
| return iceberg.GeometryTypeOf(crs) | ||||||||
| case geoarrow.EdgeVincenty, geoarrow.EdgeKarney, geoarrow.EdgeThomas, geoarrow.EdgeAndoyer, geoarrow.EdgeSpherical: | ||||||||
| return iceberg.GeographyTypeOf(crs, string(meta.Edges)) | ||||||||
| default: | ||||||||
| return nil, fmt.Errorf("unsupported geoarrow edges %q", meta.Edges) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| var authorityCodeCRS = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*:[A-Za-z0-9_.-]+$`) | ||||||||
|
|
||||||||
| func icebergCRSToGeoArrowMetadata(crs string) geoarrow.Metadata { | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small thing while we're here: |
||||||||
| lowerCRS := strings.ToLower(crs) | ||||||||
| if strings.HasPrefix(lowerCRS, "srid:") { | ||||||||
| id := crs[len("srid:"):] | ||||||||
|
|
||||||||
| if id == "0" { | ||||||||
| return geoarrow.NewMetadata() // srid:0 maps to omitted GeoArrow CRS | ||||||||
| } | ||||||||
|
|
||||||||
| raw, _ := json.Marshal(id) //nolint:errcheck // Marshalling a string can't fail | ||||||||
|
|
||||||||
| return geoarrow.Metadata{ | ||||||||
| CRS: raw, | ||||||||
| CRSType: geoarrow.CRSTypeSRID, | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| if strings.HasPrefix(lowerCRS, "projjson:") { | ||||||||
| panic(fmt.Errorf("%w: projjson CRS not supported yet", iceberg.ErrInvalidSchema)) | ||||||||
| } | ||||||||
|
|
||||||||
| var raw []byte | ||||||||
|
|
||||||||
| if lowerCRS == "epsg:4326" { | ||||||||
| // collapse EPSG:4326 to OGC:CRS84 | ||||||||
| raw, _ = json.Marshal("OGC:CRS84") //nolint:errcheck // Marshalling a string can't fail | ||||||||
| } else { | ||||||||
| raw, _ = json.Marshal(crs) //nolint:errcheck // Marshalling a string can't fail | ||||||||
| } | ||||||||
|
|
||||||||
| meta := geoarrow.Metadata{CRS: raw} | ||||||||
| if authorityCodeCRS.MatchString(crs) { | ||||||||
| meta.CRSType = geoarrow.CRSTypeAuthorityCode | ||||||||
| } | ||||||||
|
|
||||||||
| return meta | ||||||||
| } | ||||||||
Uh oh!
There was an error while loading. Please reload this page.