Skip to content

Commit

Permalink
builtins: implement ST_ForceCollection
Browse files Browse the repository at this point in the history
Release justification: low risk, high benefit changes to existing functionality

Release note (sql change): Implement the geometry builtin
`ST_ForceCollection`.
  • Loading branch information
erikgrinaker committed Aug 31, 2020
1 parent 42e73f7 commit dea9bf4
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 15 deletions.
2 changes: 2 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,8 @@ Bottom Left.</p>
</span></td></tr>
<tr><td><a name="st_force2d"></a><code>st_force2d(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry which only contains X and Y coordinates.</p>
</span></td></tr>
<tr><td><a name="st_forcecollection"></a><code>st_forcecollection(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Converts the geometry into a GeometryCollection.</p>
</span></td></tr>
<tr><td><a name="st_forcepolygonccw"></a><code>st_forcepolygonccw(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry where all Polygon objects have exterior rings in the counter-clockwise orientation and interior rings in the clockwise orientation. Non-Polygon objects are unchanged.</p>
</span></td></tr>
<tr><td><a name="st_forcepolygoncw"></a><code>st_forcepolygoncw(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a Geometry where all Polygon objects have exterior rings in the clockwise orientation and interior rings in the counter-clockwise orientation. Non-Polygon objects are unchanged.</p>
Expand Down
47 changes: 47 additions & 0 deletions pkg/geo/geomfn/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,53 @@ func collectionHomogenizeGeomT(t geom.T) (geom.T, error) {
}
}

// ForceCollection converts the input into a GeometryCollection.
func ForceCollection(g geo.Geometry) (geo.Geometry, error) {
t, err := g.AsGeomT()
if err != nil {
return geo.Geometry{}, err
}
t, err = forceCollectionFromGeomT(t)
if err != nil {
return geo.Geometry{}, err
}
return geo.MakeGeometryFromGeomT(t)
}

// forceCollectionFromGeomT converts a geom.T into a geom.GeometryCollection.
func forceCollectionFromGeomT(t geom.T) (geom.T, error) {
gc := geom.NewGeometryCollection().SetSRID(t.SRID())
switch t := t.(type) {
case *geom.Point, *geom.LineString, *geom.Polygon:
if err := gc.Push(t); err != nil {
return nil, err
}
case *geom.MultiPoint:
for i := 0; i < t.NumPoints(); i++ {
if err := gc.Push(t.Point(i)); err != nil {
return nil, err
}
}
case *geom.MultiLineString:
for i := 0; i < t.NumLineStrings(); i++ {
if err := gc.Push(t.LineString(i)); err != nil {
return nil, err
}
}
case *geom.MultiPolygon:
for i := 0; i < t.NumPolygons(); i++ {
if err := gc.Push(t.Polygon(i)); err != nil {
return nil, err
}
}
case *geom.GeometryCollection:
gc = t
default:
return nil, errors.AssertionFailedf("unknown geometry type: %T", t)
}
return gc, nil
}

// Multi converts the given geometry into a new multi-geometry.
func Multi(g geo.Geometry) (geo.Geometry, error) {
t, err := g.AsGeomT() // implicitly clones the input
Expand Down
51 changes: 51 additions & 0 deletions pkg/geo/geomfn/collections_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,57 @@ func TestCollectionHomogenize(t *testing.T) {
}
}

func TestForceCollection(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT EMPTY", "GEOMETRYCOLLECTION (POINT EMPTY)"},
{"POINT (1 2)", "GEOMETRYCOLLECTION (POINT (1 2))"},
{"MULTIPOINT EMPTY", "GEOMETRYCOLLECTION EMPTY"},
{"MULTIPOINT (1 2)", "GEOMETRYCOLLECTION (POINT (1 2))"},
{"MULTIPOINT (1 2, 3 4)", "GEOMETRYCOLLECTION (POINT (1 2), POINT (3 4))"},
{"LINESTRING EMPTY", "GEOMETRYCOLLECTION (LINESTRING EMPTY)"},
{"LINESTRING (1 2, 3 4)", "GEOMETRYCOLLECTION (LINESTRING (1 2, 3 4))"},
{"MULTILINESTRING EMPTY", "GEOMETRYCOLLECTION EMPTY"},
{"MULTILINESTRING ((1 2, 3 4))", "GEOMETRYCOLLECTION (LINESTRING (1 2, 3 4))"},
{
"MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))",
"GEOMETRYCOLLECTION (LINESTRING (1 2, 3 4), LINESTRING (5 6, 7 8))",
},
{"POLYGON EMPTY", "GEOMETRYCOLLECTION (POLYGON EMPTY)"},
{"POLYGON ((1 2, 3 4, 5 6, 1 2))", "GEOMETRYCOLLECTION (POLYGON ((1 2, 3 4, 5 6, 1 2)))"},
{"MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY"},
{"MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)))", "GEOMETRYCOLLECTION (POLYGON ((1 2, 3 4, 5 6, 1 2)))"},
{
"MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 0, 1 2, 7 8)))",
"GEOMETRYCOLLECTION (POLYGON ((1 2, 3 4, 5 6, 1 2)), POLYGON ((7 8, 9 0, 1 2, 7 8)))",
},
{"GEOMETRYCOLLECTION EMPTY", "GEOMETRYCOLLECTION EMPTY"},
{"GEOMETRYCOLLECTION (MULTIPOINT (1 1, 2 2))", "GEOMETRYCOLLECTION (MULTIPOINT (1 1, 2 2))"},
{"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION EMPTY)", "GEOMETRYCOLLECTION (GEOMETRYCOLLECTION EMPTY)"},
{
"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION(MULTIPOINT (1 1)))",
"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (MULTIPOINT (1 1)))",
},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
srid := geopb.SRID(4000)
g, err := geo.ParseGeometryFromEWKT(geopb.EWKT(tc.wkt), srid, true)
require.NoError(t, err)

result, err := ForceCollection(g)
require.NoError(t, err)
wkt, err := geo.SpatialObjectToWKT(result.SpatialObject(), 0)
require.NoError(t, err)
require.EqualValues(t, tc.expected, wkt)
require.EqualValues(t, srid, result.SRID())
})
}
}

func TestMulti(t *testing.T) {
testCases := []struct {
wkt string
Expand Down
31 changes: 16 additions & 15 deletions pkg/sql/logictest/testdata/logic_test/geospatial
Original file line number Diff line number Diff line change
Expand Up @@ -1257,26 +1257,27 @@ Square (right) POINT (0.5 0.5)
Square overlapping left and right square POINT (0.45 0.5)

# Collections
query TTT
query TTTT
SELECT
dsc,
ST_AsEWKT(ST_Multi(geom)),
ST_AsEWKT(ST_CollectionHomogenize(geom))
ST_AsText(ST_Multi(geom)),
ST_AsText(ST_CollectionHomogenize(geom)),
ST_AsText(ST_ForceCollection(geom))
FROM geom_operators_test
ORDER BY dsc ASC
----
Empty GeometryCollection GEOMETRYCOLLECTION EMPTY GEOMETRYCOLLECTION EMPTY
Empty LineString MULTILINESTRING EMPTY LINESTRING EMPTY
Empty Point MULTIPOINT EMPTY POINT EMPTY
Faraway point MULTIPOINT (5 5) POINT (5 5)
Line going through left and right square MULTILINESTRING ((-0.5 0.5, 0.5 0.5)) LINESTRING (-0.5 0.5, 0.5 0.5)
NULL NULL NULL
Nested Geometry Collection GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0))) POINT (0 0)
Point middle of Left Square MULTIPOINT (-0.5 0.5) POINT (-0.5 0.5)
Point middle of Right Square MULTIPOINT (0.5 0.5) POINT (0.5 0.5)
Square (left) MULTIPOLYGON (((-1 0, 0 0, 0 1, -1 1, -1 0))) POLYGON ((-1 0, 0 0, 0 1, -1 1, -1 0))
Square (right) MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0))) POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))
Square overlapping left and right square MULTIPOLYGON (((-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0))) POLYGON ((-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0))
Empty GeometryCollection GEOMETRYCOLLECTION EMPTY GEOMETRYCOLLECTION EMPTY GEOMETRYCOLLECTION EMPTY
Empty LineString MULTILINESTRING EMPTY LINESTRING EMPTY GEOMETRYCOLLECTION (LINESTRING EMPTY)
Empty Point MULTIPOINT EMPTY POINT EMPTY GEOMETRYCOLLECTION (POINT EMPTY)
Faraway point MULTIPOINT (5 5) POINT (5 5) GEOMETRYCOLLECTION (POINT (5 5))
Line going through left and right square MULTILINESTRING ((-0.5 0.5, 0.5 0.5)) LINESTRING (-0.5 0.5, 0.5 0.5) GEOMETRYCOLLECTION (LINESTRING (-0.5 0.5, 0.5 0.5))
NULL NULL NULL NULL
Nested Geometry Collection GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0))) POINT (0 0) GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (0 0)))
Point middle of Left Square MULTIPOINT (-0.5 0.5) POINT (-0.5 0.5) GEOMETRYCOLLECTION (POINT (-0.5 0.5))
Point middle of Right Square MULTIPOINT (0.5 0.5) POINT (0.5 0.5) GEOMETRYCOLLECTION (POINT (0.5 0.5))
Square (left) MULTIPOLYGON (((-1 0, 0 0, 0 1, -1 1, -1 0))) POLYGON ((-1 0, 0 0, 0 1, -1 1, -1 0)) GEOMETRYCOLLECTION (POLYGON ((-1 0, 0 0, 0 1, -1 1, -1 0)))
Square (right) MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0))) POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)) GEOMETRYCOLLECTION (POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))
Square overlapping left and right square MULTIPOLYGON (((-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0))) POLYGON ((-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0)) GEOMETRYCOLLECTION (POLYGON ((-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0)))

query TTTT
SELECT
Expand Down
17 changes: 17 additions & 0 deletions pkg/sql/sem/builtins/geo_builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -2413,6 +2413,23 @@ Note If the result has zero or one points, it will be returned as a POINT. If it
tree.VolatilityImmutable,
),
),
"st_forcecollection": makeBuiltin(
defProps(),
geometryOverload1(
func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) {
ret, err := geomfn.ForceCollection(g.Geometry)
if err != nil {
return nil, err
}
return &tree.DGeometry{Geometry: ret}, nil
},
types.Geometry,
infoBuilder{
info: `Converts the geometry into a GeometryCollection.`,
},
tree.VolatilityImmutable,
),
),

//
// Unary predicates
Expand Down

0 comments on commit dea9bf4

Please sign in to comment.