Repository: planetlabs/gpq (go)

Issue:
More flexibility for convert?
The new convert stuff works great. It seemed it just handles WKB, would be great if it could handle WKT as well.

The other great addition would be to enable it to use an alternate geometry column by supplying the column name - often data isn't named 'geometry'.

Pull request description:
Accept an alternative primary column name when reading Parquet
This adds an `--input-primary-column` argument to the `convert` command.  When reading a Parquet file without "geo" metadata, the primary geometry column defaults to `geometry`.  The new argument can be used to change this.

```shell
gpq convert --input-primary-column geom example.parquet example.geoparquet
```

Fixes #33.

Patch:
diff --git a/cmd/gpq/convert.go b/cmd/gpq/convert.go
index cb0431a..8aed67e 100644
--- a/cmd/gpq/convert.go
+++ b/cmd/gpq/convert.go
@@ -26,13 +26,14 @@ import (
 )
 
 type ConvertCmd struct {
-	Input       string `arg:"" name:"input" help:"Input file." type:"existingfile"`
-	From        string `help:"Input file format.  Possible values: ${enum}." enum:"auto, geojson, geoparquet, parquet" default:"auto"`
-	Output      string `arg:"" name:"output" help:"Output file." type:"path"`
-	To          string `help:"Output file format.  Possible values: ${enum}." enum:"auto, geojson, geoparquet" default:"auto"`
-	Min         int    `help:"Minimum number of features to consider when building a schema." default:"10"`
-	Max         int    `help:"Maximum number of features to consider when building a schema." default:"100"`
-	Compression string `help:"Parquet compression to use.  Possible values: ${enum}." enum:"uncompressed, snappy, gzip, brotli, zstd, lz4raw" default:"gzip"`
+	Input              string `arg:"" name:"input" help:"Input file." type:"existingfile"`
+	From               string `help:"Input file format.  Possible values: ${enum}." enum:"auto, geojson, geoparquet, parquet" default:"auto"`
+	Output             string `arg:"" name:"output" help:"Output file." type:"path"`
+	To                 string `help:"Output file format.  Possible values: ${enum}." enum:"auto, geojson, geoparquet" default:"auto"`
+	Min                int    `help:"Minimum number of features to consider when building a schema." default:"10"`
+	Max                int    `help:"Maximum number of features to consider when building a schema." default:"100"`
+	InputPrimaryColumn string `help:"Primary geometry column name when reading Parquet withtout metadata." default:"geometry"`
+	Compression        string `help:"Parquet compression to use.  Possible values: ${enum}." enum:"uncompressed, snappy, gzip, brotli, zstd, lz4raw" default:"gzip"`
 }
 
 type FormatType string
@@ -124,5 +125,11 @@ func (c *ConvertCmd) Run() error {
 		return geojson.FromParquet(file, output)
 	}
 
-	return geoparquet.FromParquet(file, output, nil)
+	var convertOptions *geoparquet.ConvertOptions
+	if c.InputPrimaryColumn != geoparquet.DefaultGeometryColumn {
+		convertOptions = &geoparquet.ConvertOptions{
+			InputPrimaryColumn: c.InputPrimaryColumn,
+		}
+	}
+	return geoparquet.FromParquet(file, output, convertOptions)
 }
diff --git a/internal/geoparquet/geoparquet.go b/internal/geoparquet/geoparquet.go
index 66d582d..af1b8c2 100644
--- a/internal/geoparquet/geoparquet.go
+++ b/internal/geoparquet/geoparquet.go
@@ -35,7 +35,7 @@ const (
 	EdgesPlanar                 = "planar"
 	EdgesSpherical              = "spherical"
 	OrientationCounterClockwise = "counterclockwise"
-	defaultGeometryColumn       = "geometry"
+	DefaultGeometryColumn       = "geometry"
 )
 
 var GeometryTypes = []string{
@@ -134,9 +134,9 @@ func getDefaultGeometryColumn() *GeometryColumn {
 func DefaultMetadata() *Metadata {
 	return &Metadata{
 		Version:       Version,
-		PrimaryColumn: defaultGeometryColumn,
+		PrimaryColumn: DefaultGeometryColumn,
 		Columns: map[string]*GeometryColumn{
-			defaultGeometryColumn: getDefaultGeometryColumn(),
+			DefaultGeometryColumn: getDefaultGeometryColumn(),
 		},
 	}
 }
@@ -330,7 +330,8 @@ func GetCodec(codec string) (compress.Codec, error) {
 }
 
 type ConvertOptions struct {
-	Compression string
+	InputPrimaryColumn string
+	Compression        string
 }
 
 func FromParquet(file *parquet.File, output io.Writer, convertOptions *ConvertOptions) error {
@@ -367,10 +368,14 @@ func FromParquet(file *parquet.File, output io.Writer, convertOptions *ConvertOp
 
 	inputMetadata, metadataErr := GetMetadata(file)
 	if metadataErr != nil {
+		primaryColumn := DefaultGeometryColumn
+		if convertOptions.InputPrimaryColumn != "" {
+			primaryColumn = convertOptions.InputPrimaryColumn
+		}
 		inputMetadata = &Metadata{
-			PrimaryColumn: defaultGeometryColumn,
+			PrimaryColumn: primaryColumn,
 			Columns: map[string]*GeometryColumn{
-				defaultGeometryColumn: {},
+				primaryColumn: {},
 			},
 		}
 	}
diff --git a/readme.md b/readme.md
index 99c4877..542d7c0 100644
--- a/readme.md
+++ b/readme.md
@@ -69,6 +69,8 @@ gpq convert non-geo.parquet valid-geo.parquet
 
 When reading from a Parquet file and writing out GeoParquet, the input geometry values can be WKB or WKT encoded.  The output geometry values will always be WKB encoded.
 
+The `--input-primary-column` argument can be used to provide a primary geometry column name when reading Parquet files without "geo" metadata (defaults to `geometry`).
+
 The `--compression` argument can be used to control the compression codec used when writing GeoParquet.  See `gpq convert --help` for the available options.
 
 


Test patch:
diff --git a/internal/geoparquet/geoparquet_test.go b/internal/geoparquet/geoparquet_test.go
index 974362f..dd3d621 100644
--- a/internal/geoparquet/geoparquet_test.go
+++ b/internal/geoparquet/geoparquet_test.go
@@ -299,3 +299,50 @@ func TestFromParquetWithWKT(t *testing.T) {
 
 	assert.Equal(t, int64(2), geoparquetFile.NumRows())
 }
+
+func TestFromParquetWithAltPrimaryColumn(t *testing.T) {
+	type Row struct {
+		Name string `parquet:"name"`
+		Geo  string `parquet:"geo"`
+	}
+
+	rows := []*Row{
+		{
+			Name: "test-point-1",
+			Geo:  string(wkt.Marshal(orb.Point{1, 2})),
+		},
+		{
+			Name: "test-point-2",
+			Geo:  string(wkt.Marshal(orb.Point{3, 4})),
+		},
+	}
+
+	parquetFile, inputErr := makeParquet(rows, nil)
+	require.NoError(t, inputErr)
+
+	output := &bytes.Buffer{}
+	convertErr := geoparquet.FromParquet(parquetFile, output, &geoparquet.ConvertOptions{InputPrimaryColumn: "geo"})
+	require.NoError(t, convertErr)
+
+	geoparquetInput := bytes.NewReader(output.Bytes())
+	geoparquetFile, outputErr := parquet.OpenFile(geoparquetInput, geoparquetInput.Size())
+	require.NoError(t, outputErr)
+
+	metadata, geoErr := geoparquet.GetMetadata(geoparquetFile)
+	require.NoError(t, geoErr)
+
+	assert.Len(t, metadata.Columns, 1)
+
+	primaryColumnMetadata := metadata.Columns[metadata.PrimaryColumn]
+
+	geometryTypes := primaryColumnMetadata.GetGeometryTypes()
+	assert.Len(t, geometryTypes, 1)
+	assert.Contains(t, geometryTypes, "Point")
+
+	bounds := primaryColumnMetadata.Bounds
+	assert.Equal(t, []float64{1, 2, 3, 4}, bounds)
+
+	assert.Equal(t, geoparquet.EncodingWKB, primaryColumnMetadata.Encoding)
+
+	assert.Equal(t, int64(2), geoparquetFile.NumRows())
+}