Repository: planetlabs/gpq
Pull request #16 (2023-05-11 20:13:20)

Support WKT encoding and use column type when encoding is unknown
This adds support for WKT encoded geometries.

Although the spec doesn't currently allow it, if `"encoding": "WKT"` metadata is included for a geometry column, the data will be read as WKT.

In addition, if no `encoding` metadata property is found for a geometry column, the encoding will be derived from the column type.  For columns annotated with the string logical type, WKT will be assumed.  For other byte arrays (with no string annotation), WKB will be assumed.


Patch:
diff --git a/cmd/gpq/describe.go b/cmd/gpq/describe.go
index e78d41f..545345a 100644
--- a/cmd/gpq/describe.go
+++ b/cmd/gpq/describe.go
@@ -45,14 +45,14 @@ func (c *DescribeCmd) Run() error {
 		return fileErr
 	}
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(file)
+	metadata, geoErr := geoparquet.GetMetadata(file)
 	if geoErr != nil {
 		return geoErr
 	}
 
 	info := &Info{
 		Schema:   buildSchema("", file.Schema()),
-		Metadata: geoMetadata,
+		Metadata: metadata,
 	}
 
 	encoder := json.NewEncoder(os.Stdout)
@@ -68,8 +68,8 @@ func (c *DescribeCmd) Run() error {
 }
 
 type Info struct {
-	Schema   *Schema                 `json:"schema"`
-	Metadata *geoparquet.GeoMetadata `json:"metadata"`
+	Schema   *Schema              `json:"schema"`
+	Metadata *geoparquet.Metadata `json:"metadata"`
 }
 
 type Schema struct {
diff --git a/cmd/wasm/main.go b/cmd/wasm/main.go
index 61db93b..2834260 100644
--- a/cmd/wasm/main.go
+++ b/cmd/wasm/main.go
@@ -67,11 +67,11 @@ var fromParquet = js.FuncOf(func(this js.Value, args []js.Value) any {
 		return returnFromError(convertErr)
 	}
 
-	geoMetadata, _ := input.Lookup("geo")
+	metadata, _ := input.Lookup("geo")
 
 	return returnFromValue(map[string]any{
 		"data":    output.String(),
-		"geo":     geoMetadata,
+		"geo":     metadata,
 		"schema":  input.Schema().String(),
 		"records": input.NumRows(),
 	})
@@ -98,14 +98,14 @@ var toParquet = js.FuncOf(func(this js.Value, args []js.Value) any {
 		return returnFromError(err)
 	}
 
-	geoMetadata, _ := file.Lookup("geo")
+	metadata, _ := file.Lookup("geo")
 
 	array := uint8ArrayConstructor.New(output.Len())
 	js.CopyBytesToJS(array, output.Bytes())
 
 	return returnFromValue(map[string]any{
 		"data":    array,
-		"geo":     geoMetadata,
+		"geo":     metadata,
 		"schema":  file.Schema().String(),
 		"records": file.NumRows(),
 	})
diff --git a/internal/geojson/geojson.go b/internal/geojson/geojson.go
index 21866c9..387b9c6 100644
--- a/internal/geojson/geojson.go
+++ b/internal/geojson/geojson.go
@@ -20,89 +20,126 @@ import (
 	"fmt"
 	"io"
 	"reflect"
+	"strings"
 
 	"github.com/paulmach/orb"
 	"github.com/paulmach/orb/encoding/wkb"
+	"github.com/paulmach/orb/encoding/wkt"
 	orbjson "github.com/paulmach/orb/geojson"
 	"github.com/planetlabs/gpq/internal/geoparquet"
 	"github.com/segmentio/parquet-go"
 )
 
 type FeatureWriter struct {
-	writer      io.Writer
-	schema      *parquet.Schema
-	geoMetadata *geoparquet.GeoMetadata
-	writing     bool
+	writer   io.Writer
+	schema   *parquet.Schema
+	metadata *geoparquet.Metadata
+	writing  bool
 }
 
-func NewFeatureWriter(writer io.Writer, geoMetadata *geoparquet.GeoMetadata, schema *parquet.Schema) (*FeatureWriter, error) {
+func NewFeatureWriter(writer io.Writer, metadata *geoparquet.Metadata, schema *parquet.Schema) (*FeatureWriter, error) {
 	featureWriter := &FeatureWriter{
-		writer:      writer,
-		schema:      schema,
-		geoMetadata: geoMetadata,
+		writer:   writer,
+		schema:   schema,
+		metadata: metadata,
 	}
 	return featureWriter, nil
 }
 
-func (w *FeatureWriter) Write(row parquet.Row) error {
-	if !w.writing {
-		_, err := io.WriteString(w.writer, `{"type":"FeatureCollection","features":[`)
-		if err != nil {
-			return err
-		}
-		w.writing = true
-	} else {
-		_, err := io.WriteString(w.writer, ",")
-		if err != nil {
-			return err
-		}
-	}
+var stringType = parquet.String().Type()
 
+func toFeature(row parquet.Row, schema *parquet.Schema, metadata *geoparquet.Metadata) (*Feature, error) {
 	properties := map[string]any{}
-	if err := w.schema.Reconstruct(&properties, row); err != nil {
-		return err
+	if err := schema.Reconstruct(&properties, row); err != nil {
+		return nil, err
 	}
 
-	var geometry orb.Geometry
-	geometryInterface, ok := properties[w.geoMetadata.PrimaryColumn]
-	if ok && geometryInterface != nil {
-		geometryString, ok := geometryInterface.(string)
-		if !ok {
-			return fmt.Errorf("unexpected geometry type: %t", geometryInterface)
-		}
-		g, err := wkb.Unmarshal([]byte(geometryString))
-		if err != nil {
-			return fmt.Errorf("trouble reading geometry: %w", err)
-		}
-		geometry = g
-	}
-	for geometryName := range w.geoMetadata.Columns {
+	var primaryGeometry orb.Geometry
+
+	for geometryName := range metadata.Columns {
 		geometryInterface, ok := properties[geometryName]
 		if !ok {
-			continue
+			return nil, fmt.Errorf("missing geometry column: %s", geometryName)
+		}
+
+		if geometryName == metadata.PrimaryColumn {
+			delete(properties, geometryName)
 		}
-		delete(properties, geometryName)
 		if geometryInterface == nil {
 			continue
 		}
+
 		geometryString, ok := geometryInterface.(string)
 		if !ok {
-			return fmt.Errorf("unexpected geometry type: %t", geometryInterface)
+			return nil, fmt.Errorf("unexpected geometry type: %t", geometryInterface)
 		}
-		g, err := wkb.Unmarshal([]byte(geometryString))
-		if err != nil {
-			return fmt.Errorf("trouble reading geometry: %w", err)
+
+		encoding := metadata.Columns[geometryName].Encoding
+		if encoding == "" {
+			column, ok := schema.Lookup(geometryName)
+			if !ok {
+				return nil, fmt.Errorf("missing column: %s", geometryName)
+			}
+			nodeType := column.Node.Type()
+			if nodeType == stringType {
+				encoding = geoparquet.EncodingWKT
+			} else if nodeType == parquet.ByteArrayType {
+				encoding = geoparquet.EncodingWKB
+			} else {
+				return nil, fmt.Errorf("unsupported geometry type: %s", nodeType)
+			}
 		}
-		if geometryName == w.geoMetadata.PrimaryColumn {
+
+		var geometry orb.Geometry
+
+		switch strings.ToUpper(encoding) {
+		case geoparquet.EncodingWKB:
+			g, err := wkb.Unmarshal([]byte(geometryString))
+			if err != nil {
+				return nil, fmt.Errorf("trouble reading geometry: %w", err)
+			}
+			geometry = g
+		case geoparquet.EncodingWKT:
+			g, err := wkt.Unmarshal(geometryString)
+			if err != nil {
+				return nil, fmt.Errorf("trouble reading geometry: %w", err)
+			}
 			geometry = g
+		default:
+			return nil, fmt.Errorf("unsupported encoding: %s", encoding)
+		}
+
+		if geometryName == metadata.PrimaryColumn {
+			primaryGeometry = geometry
 		} else {
-			properties[geometryName] = g
+			properties[geometryName] = geometry
 		}
 	}
 
 	feature := &Feature{
 		Properties: properties,
-		Geometry:   geometry,
+		Geometry:   primaryGeometry,
+	}
+	return feature, nil
+}
+
+func (w *FeatureWriter) Write(row parquet.Row) error {
+	if !w.writing {
+		_, err := io.WriteString(w.writer, `{"type":"FeatureCollection","features":[`)
+		if err != nil {
+			return err
+		}
+		w.writing = true
+	} else {
+		_, err := io.WriteString(w.writer, ",")
+		if err != nil {
+			return err
+		}
+	}
+
+	feature, err := toFeature(row, w.schema, w.metadata)
+	if err != nil {
+		return err
 	}
 
 	encoder := json.NewEncoder(w.writer)
@@ -190,12 +227,12 @@ func (f *Feature) UnmarshalJSON(data []byte) error {
 func FromParquet(file *parquet.File, writer io.Writer) error {
 	rowReader := geoparquet.NewRowReader(file)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(file)
+	metadata, geoErr := geoparquet.GetMetadata(file)
 	if geoErr != nil {
 		return geoErr
 	}
 
-	featureWriter, writerErr := NewFeatureWriter(writer, geoMetadata, file.Schema())
+	featureWriter, writerErr := NewFeatureWriter(writer, metadata, file.Schema())
 	if writerErr != nil {
 		return writerErr
 	}
@@ -579,9 +616,9 @@ func ToParquet(input io.Reader, output io.Writer, convertOptions *ConvertOptions
 		}
 	}
 
-	geoMetadata := GetDefaultMetadata()
+	metadata := GetDefaultMetadata()
 	if bounds != nil {
-		geoMetadata.Columns[geoMetadata.PrimaryColumn].Bounds = []float64{
+		metadata.Columns[metadata.PrimaryColumn].Bounds = []float64{
 			bounds.Left(), bounds.Bottom(), bounds.Right(), bounds.Top(),
 		}
 	}
@@ -592,13 +629,13 @@ func ToParquet(input io.Reader, output io.Writer, convertOptions *ConvertOptions
 			geometryTypes = append(geometryTypes, geometryType)
 		}
 	}
-	geoMetadata.Columns[geoMetadata.PrimaryColumn].GeometryTypes = geometryTypes
+	metadata.Columns[metadata.PrimaryColumn].GeometryTypes = geometryTypes
 
-	geoMetadataEncoded, jsonErr := json.Marshal(geoMetadata)
+	metadataEncoded, jsonErr := json.Marshal(metadata)
 	if jsonErr != nil {
 		return fmt.Errorf("failed to serialize geo metadata: %w", jsonErr)
 	}
 
-	writer.SetKeyValueMetadata(geoparquet.GeoMetadataKey, string(geoMetadataEncoded))
+	writer.SetKeyValueMetadata(geoparquet.MetadataKey, string(metadataEncoded))
 	return writer.Close()
 }
diff --git a/internal/geojson/metadata.go b/internal/geojson/metadata.go
index 947900a..9efde1a 100644
--- a/internal/geojson/metadata.go
+++ b/internal/geojson/metadata.go
@@ -20,8 +20,8 @@ import (
 
 const primaryColumn = "geometry"
 
-func GetDefaultMetadata() *geoparquet.GeoMetadata {
-	return &geoparquet.GeoMetadata{
+func GetDefaultMetadata() *geoparquet.Metadata {
+	return &geoparquet.Metadata{
 		Version:       geoparquet.Version,
 		PrimaryColumn: primaryColumn,
 		Columns: map[string]*geoparquet.GeometryColumn{
diff --git a/internal/geoparquet/geoparquet.go b/internal/geoparquet/geoparquet.go
index cd84807..aee8317 100644
--- a/internal/geoparquet/geoparquet.go
+++ b/internal/geoparquet/geoparquet.go
@@ -22,11 +22,15 @@ import (
 	"github.com/segmentio/parquet-go"
 )
 
-const GeoMetadataKey = "geo"
-
-const Version = "1.0.0-beta.1"
+const (
+	Version               = "1.0.0-beta.1"
+	MetadataKey           = "geo"
+	EncodingWKB           = "WKB"
+	EncodingWKT           = "WKT"
+	defaultGeometryColumn = "geometry"
+)
 
-type GeoMetadata struct {
+type Metadata struct {
 	Version       string                     `json:"version"`
 	PrimaryColumn string                     `json:"primary_column"`
 	Columns       map[string]*GeometryColumn `json:"columns"`
@@ -77,20 +81,35 @@ func (col *GeometryColumn) GetGeometryTypes() []string {
 	return types
 }
 
-func GetGeoMetadataValue(file *parquet.File) (string, error) {
-	value, ok := file.Lookup(GeoMetadataKey)
+func DefaultMetadata() *Metadata {
+	return &Metadata{
+		Version:       Version,
+		PrimaryColumn: defaultGeometryColumn,
+		Columns: map[string]*GeometryColumn{
+			defaultGeometryColumn: {
+				Encoding:      EncodingWKB,
+				GeometryTypes: []string{},
+			},
+		},
+	}
+}
+
+var ErrNoMetadata = fmt.Errorf("missing %s metadata key", MetadataKey)
+
+func GetMetadataValue(file *parquet.File) (string, error) {
+	value, ok := file.Lookup(MetadataKey)
 	if !ok {
-		return "", fmt.Errorf("missing %s metadata key", GeoMetadataKey)
+		return "", ErrNoMetadata
 	}
 	return value, nil
 }
 
-func GetGeoMetadata(file *parquet.File) (*GeoMetadata, error) {
-	value, valueErr := GetGeoMetadataValue(file)
+func GetMetadata(file *parquet.File) (*Metadata, error) {
+	value, valueErr := GetMetadataValue(file)
 	if valueErr != nil {
 		return nil, valueErr
 	}
-	geoFileMetadata := &GeoMetadata{}
+	geoFileMetadata := &Metadata{}
 	jsonErr := json.Unmarshal([]byte(value), geoFileMetadata)
 	if jsonErr != nil {
 		return nil, fmt.Errorf("unable to parse geo metadata: %w", jsonErr)
diff --git a/internal/validator/validator.go b/internal/validator/validator.go
index 576ff6f..336d4e0 100644
--- a/internal/validator/validator.go
+++ b/internal/validator/validator.go
@@ -80,7 +80,7 @@ func (v *Validator) Validate(ctx context.Context, resource string) error {
 		return fileErr
 	}
 
-	value, geoErr := geoparquet.GetGeoMetadataValue(file)
+	value, geoErr := geoparquet.GetMetadataValue(file)
 	if geoErr != nil {
 		return geoErr
 	}


Test patch:
diff --git a/internal/geojson/geojson_test.go b/internal/geojson/geojson_test.go
index 258b38c..5f00321 100644
--- a/internal/geojson/geojson_test.go
+++ b/internal/geojson/geojson_test.go
@@ -16,11 +16,14 @@ package geojson_test
 
 import (
 	"bytes"
+	"encoding/json"
+	"fmt"
 	"io"
 	"os"
 	"testing"
 
 	"github.com/paulmach/orb"
+	"github.com/paulmach/orb/encoding/wkb"
 	"github.com/planetlabs/gpq/internal/geojson"
 	"github.com/planetlabs/gpq/internal/geoparquet"
 	"github.com/segmentio/parquet-go"
@@ -210,17 +213,17 @@ func TestToParquet(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	geometryTypes := geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes()
+	geometryTypes := metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes()
 	assert.Len(t, geometryTypes, 2)
 	assert.Contains(t, geometryTypes, "MultiPolygon")
 	assert.Contains(t, geometryTypes, "Polygon")
 
-	assert.Nil(t, geoMetadata.Columns[geoMetadata.PrimaryColumn].GeometryType)
+	assert.Nil(t, metadata.Columns[metadata.PrimaryColumn].GeometryType)
 
-	gotBounds := geoMetadata.Columns[geoMetadata.PrimaryColumn].Bounds
+	gotBounds := metadata.Columns[metadata.PrimaryColumn].Bounds
 	assert.Equal(t, []float64{-180, -18.28799, 180, 83.23324000000001}, gotBounds)
 
 	assert.Equal(t, int64(5), parquetFile.NumRows())
@@ -306,12 +309,12 @@ func TestToParquetAllNullGeometry(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	assert.Len(t, geoMetadata.Columns[geoMetadata.PrimaryColumn].GeometryTypes, 0)
-	assert.Nil(t, geoMetadata.Columns[geoMetadata.PrimaryColumn].GeometryType)
-	assert.Len(t, geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes(), 0)
+	assert.Len(t, metadata.Columns[metadata.PrimaryColumn].GeometryTypes, 0)
+	assert.Nil(t, metadata.Columns[metadata.PrimaryColumn].GeometryType)
+	assert.Len(t, metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes(), 0)
 
 	schema := parquetFile.Schema()
 
@@ -338,10 +341,10 @@ func TestToParqueStringId(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	geometryTypes := geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes()
+	geometryTypes := metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes()
 	assert.Equal(t, []string{"Point"}, geometryTypes)
 }
 
@@ -357,10 +360,10 @@ func TestToParqueNumberId(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	geometryTypes := geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes()
+	geometryTypes := metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes()
 	assert.Equal(t, []string{"Point"}, geometryTypes)
 }
 
@@ -403,10 +406,10 @@ func TestToParquetWithCRS(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	geometryTypes := geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes()
+	geometryTypes := metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes()
 	assert.Equal(t, []string{"Polygon"}, geometryTypes)
 }
 
@@ -422,10 +425,10 @@ func TestToParquetExtraArray(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	geometryTypes := geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes()
+	geometryTypes := metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes()
 	assert.Equal(t, []string{"Point"}, geometryTypes)
 
 	schema := parquetFile.Schema()
@@ -453,10 +456,10 @@ func TestToParquetExtraObject(t *testing.T) {
 	parquetFile, openErr := parquet.OpenFile(parquetInput, parquetInput.Size())
 	require.NoError(t, openErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(parquetFile)
+	metadata, geoErr := geoparquet.GetMetadata(parquetFile)
 	require.NoError(t, geoErr)
 
-	geometryTypes := geoMetadata.Columns[geoMetadata.PrimaryColumn].GetGeometryTypes()
+	geometryTypes := metadata.Columns[metadata.PrimaryColumn].GetGeometryTypes()
 	assert.Equal(t, []string{"Point"}, geometryTypes)
 
 	schema := parquetFile.Schema()
@@ -555,3 +558,214 @@ func TestRoundTripSparseProperties(t *testing.T) {
 
 	assert.JSONEq(t, string(inputData), jsonBuffer.String())
 }
+
+func makeGeoParquet[T any](rows []T, metadata *geoparquet.Metadata) (*parquet.File, error) {
+	data := &bytes.Buffer{}
+
+	writer := parquet.NewGenericWriter[T](data)
+
+	_, writeErr := writer.Write(rows)
+	if writeErr != nil {
+		return nil, fmt.Errorf("trouble writing rows: %w", writeErr)
+	}
+
+	jsonMetadata, jsonErr := json.Marshal(metadata)
+	if jsonErr != nil {
+		return nil, fmt.Errorf("trouble encoding metadata as json: %w", jsonErr)
+	}
+
+	writer.SetKeyValueMetadata(geoparquet.MetadataKey, string(jsonMetadata))
+	closeErr := writer.Close()
+	if closeErr != nil {
+		return nil, fmt.Errorf("trouble closing writer: %w", closeErr)
+	}
+
+	return parquet.OpenFile(bytes.NewReader(data.Bytes()), int64(data.Len()))
+}
+
+func TestWKT(t *testing.T) {
+	type RowType struct {
+		Name     string `parquet:"name"`
+		Geometry string `parquet:"geometry"`
+	}
+
+	rows := []*RowType{
+		{
+			Name:     "test-point",
+			Geometry: "POINT (1 2)",
+		},
+		{
+			Name:     "test-line",
+			Geometry: "LINESTRING (30 10, 10 30, 40 40)",
+		},
+	}
+
+	metadata := geoparquet.DefaultMetadata()
+	metadata.Columns[metadata.PrimaryColumn].Encoding = geoparquet.EncodingWKT
+
+	file, fileErr := makeGeoParquet(rows, metadata)
+	require.NoError(t, fileErr)
+
+	output := &bytes.Buffer{}
+	convertErr := geojson.FromParquet(file, output)
+	require.NoError(t, convertErr)
+
+	expected := `{
+		"type": "FeatureCollection",
+		"features": [
+			{
+				"type": "Feature",
+				"properties": {
+					"name": "test-point"
+				},
+				"geometry": {
+					"type": "Point",
+					"coordinates": [1, 2]
+				}
+			},
+			{
+				"type": "Feature",
+				"properties": {
+					"name": "test-line"
+				},
+				"geometry": {
+					"type": "LineString",
+					"coordinates": [[30, 10], [10, 30], [40, 40]]
+				}
+			}
+		]
+	}`
+
+	assert.JSONEq(t, expected, output.String())
+}
+
+func TestWKTNoEncoding(t *testing.T) {
+	type RowType struct {
+		Name     string `parquet:"name"`
+		Geometry string `parquet:"geometry"`
+	}
+
+	rows := []*RowType{
+		{
+			Name:     "test-point",
+			Geometry: "POINT (1 2)",
+		},
+	}
+
+	metadata := geoparquet.DefaultMetadata()
+	metadata.Columns[metadata.PrimaryColumn].Encoding = ""
+
+	file, fileErr := makeGeoParquet(rows, metadata)
+	require.NoError(t, fileErr)
+
+	output := &bytes.Buffer{}
+	convertErr := geojson.FromParquet(file, output)
+	require.NoError(t, convertErr)
+
+	expected := `{
+		"type": "FeatureCollection",
+		"features": [
+			{
+				"type": "Feature",
+				"properties": {
+					"name": "test-point"
+				},
+				"geometry": {
+					"type": "Point",
+					"coordinates": [1, 2]
+				}
+			}
+		]
+	}`
+
+	assert.JSONEq(t, expected, output.String())
+}
+
+func TestWKB(t *testing.T) {
+	type RowType struct {
+		Name     string `parquet:"name"`
+		Geometry []byte `parquet:"geometry"`
+	}
+
+	point, pointErr := wkb.Marshal(orb.Point{1, 2})
+	require.NoError(t, pointErr)
+
+	rows := []*RowType{
+		{
+			Name:     "test-point",
+			Geometry: point,
+		},
+	}
+
+	metadata := geoparquet.DefaultMetadata()
+
+	file, fileErr := makeGeoParquet(rows, metadata)
+	require.NoError(t, fileErr)
+
+	output := &bytes.Buffer{}
+	convertErr := geojson.FromParquet(file, output)
+	require.NoError(t, convertErr)
+
+	expected := `{
+		"type": "FeatureCollection",
+		"features": [
+			{
+				"type": "Feature",
+				"properties": {
+					"name": "test-point"
+				},
+				"geometry": {
+					"type": "Point",
+					"coordinates": [1, 2]
+				}
+			}
+		]
+	}`
+
+	assert.JSONEq(t, expected, output.String())
+}
+
+func TestWKBNoEncoding(t *testing.T) {
+	type RowType struct {
+		Name     string `parquet:"name"`
+		Geometry []byte `parquet:"geometry"`
+	}
+
+	point, pointErr := wkb.Marshal(orb.Point{1, 2})
+	require.NoError(t, pointErr)
+
+	rows := []*RowType{
+		{
+			Name:     "test-point",
+			Geometry: point,
+		},
+	}
+
+	metadata := geoparquet.DefaultMetadata()
+	metadata.Columns[metadata.PrimaryColumn].Encoding = ""
+
+	file, fileErr := makeGeoParquet(rows, metadata)
+	require.NoError(t, fileErr)
+
+	output := &bytes.Buffer{}
+	convertErr := geojson.FromParquet(file, output)
+	require.NoError(t, convertErr)
+
+	expected := `{
+		"type": "FeatureCollection",
+		"features": [
+			{
+				"type": "Feature",
+				"properties": {
+					"name": "test-point"
+				},
+				"geometry": {
+					"type": "Point",
+					"coordinates": [1, 2]
+				}
+			}
+		]
+	}`
+
+	assert.JSONEq(t, expected, output.String())
+}
diff --git a/internal/geoparquet/geoparquet_test.go b/internal/geoparquet/geoparquet_test.go
index 93e7bb3..a30d7e0 100644
--- a/internal/geoparquet/geoparquet_test.go
+++ b/internal/geoparquet/geoparquet_test.go
@@ -25,7 +25,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestGetGeoMetadataV040(t *testing.T) {
+func TestGetMetadataV040(t *testing.T) {
 	fixturePath := "../testdata/cases/example-v0.4.0.parquet"
 	info, statErr := os.Stat(fixturePath)
 	require.NoError(t, statErr)
@@ -36,21 +36,21 @@ func TestGetGeoMetadataV040(t *testing.T) {
 	file, fileErr := parquet.OpenFile(input, info.Size())
 	require.NoError(t, fileErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(file)
+	metadata, geoErr := geoparquet.GetMetadata(file)
 	require.NoError(t, geoErr)
 
-	assert.Equal(t, "geometry", geoMetadata.PrimaryColumn)
-	assert.Equal(t, "0.4.0", geoMetadata.Version)
-	require.Len(t, geoMetadata.Columns, 1)
+	assert.Equal(t, "geometry", metadata.PrimaryColumn)
+	assert.Equal(t, "0.4.0", metadata.Version)
+	require.Len(t, metadata.Columns, 1)
 
-	col := geoMetadata.Columns[geoMetadata.PrimaryColumn]
+	col := metadata.Columns[metadata.PrimaryColumn]
 	assert.Equal(t, "WKB", col.Encoding)
 	assert.Equal(t, "planar", col.Edges)
 	assert.Equal(t, []float64{-180, -90, 180, 83.6451}, col.Bounds)
 	assert.Equal(t, []string{"Polygon", "MultiPolygon"}, col.GetGeometryTypes())
 }
 
-func TestGetGeoMetadataV100Beta1(t *testing.T) {
+func TestGetMetadataV100Beta1(t *testing.T) {
 	fixturePath := "../testdata/cases/example-v1.0.0-beta.1.parquet"
 	info, statErr := os.Stat(fixturePath)
 	require.NoError(t, statErr)
@@ -61,14 +61,14 @@ func TestGetGeoMetadataV100Beta1(t *testing.T) {
 	file, fileErr := parquet.OpenFile(input, info.Size())
 	require.NoError(t, fileErr)
 
-	geoMetadata, geoErr := geoparquet.GetGeoMetadata(file)
+	metadata, geoErr := geoparquet.GetMetadata(file)
 	require.NoError(t, geoErr)
 
-	assert.Equal(t, "geometry", geoMetadata.PrimaryColumn)
-	assert.Equal(t, "1.0.0-beta.1", geoMetadata.Version)
-	require.Len(t, geoMetadata.Columns, 1)
+	assert.Equal(t, "geometry", metadata.PrimaryColumn)
+	assert.Equal(t, "1.0.0-beta.1", metadata.Version)
+	require.Len(t, metadata.Columns, 1)
 
-	col := geoMetadata.Columns[geoMetadata.PrimaryColumn]
+	col := metadata.Columns[metadata.PrimaryColumn]
 	assert.Equal(t, "WKB", col.Encoding)
 	assert.Equal(t, "planar", col.Edges)
 	assert.Equal(t, []float64{-180, -90, 180, 83.6451}, col.Bounds)