diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 450a6127dc53..e8c6717ecd97 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -1541,6 +1541,8 @@ calculated, the result is transformed back into a Geography with SRID 4326.
st_pointonsurface(geometry: geometry) → geometry
Returns a point that intersects with the given Geometry.
This function utilizes the GEOS module.
st_points(geometry: geometry) → geometry
Returns all coordinates in the given Geometry as a MultiPoint, including duplicates.
+st_polyfromtext(str: string, srid: int) → geometry
Returns the Geometry from a WKT or EWKT representation with an SRID. If the shape underneath is not Polygon, NULL is returned. If the SRID is present in both the EWKT and the argument, the argument value is used.
st_polyfromtext(val: string) → geometry
Returns the Geometry from a WKT or EWKT representation. If the shape underneath is not Polygon, NULL is returned.
diff --git a/pkg/geo/geomfn/unary_operators.go b/pkg/geo/geomfn/unary_operators.go index 4baef8c7ff5b..6b868862b063 100644 --- a/pkg/geo/geomfn/unary_operators.go +++ b/pkg/geo/geomfn/unary_operators.go @@ -148,3 +148,61 @@ func dimensionFromGeomT(geomRepr geom.T) (int, error) { return 0, errors.AssertionFailedf("unknown geometry type: %T", geomRepr) } } + +// Points returns the points of all coordinates in a geometry as a multipoint. +func Points(g geo.Geometry) (geo.Geometry, error) { + t, err := g.AsGeomT() + if err != nil { + return geo.Geometry{}, err + } + layout := t.Layout() + if gc, ok := t.(*geom.GeometryCollection); ok && gc.Empty() { + layout = geom.XY + } + points := geom.NewMultiPoint(layout).SetSRID(t.SRID()) + iter := geo.NewGeomTIterator(t, geo.EmptyBehaviorOmit) + for { + geomRepr, hasNext, err := iter.Next() + if err != nil { + return geo.Geometry{}, err + } else if !hasNext { + break + } else if geomRepr.Empty() { + continue + } + switch geomRepr := geomRepr.(type) { + case *geom.Point: + if err = pushCoord(points, geomRepr.Coords()); err != nil { + return geo.Geometry{}, err + } + case *geom.LineString: + for i := 0; i < geomRepr.NumCoords(); i++ { + if err = pushCoord(points, geomRepr.Coord(i)); err != nil { + return geo.Geometry{}, err + } + } + case *geom.Polygon: + for i := 0; i < geomRepr.NumLinearRings(); i++ { + linearRing := geomRepr.LinearRing(i) + for j := 0; j < linearRing.NumCoords(); j++ { + if err = pushCoord(points, linearRing.Coord(j)); err != nil { + return geo.Geometry{}, err + } + } + } + default: + return geo.Geometry{}, errors.AssertionFailedf("unexpected type: %T", geomRepr) + } + } + return geo.MakeGeometryFromGeomT(points) +} + +// pushCoord is a helper function for PointsFromGeomT that appends +// a coordinate to a multipoint as a point. +func pushCoord(points *geom.MultiPoint, coord geom.Coord) error { + point, err := geom.NewPoint(points.Layout()).SetCoords(coord) + if err != nil { + return err + } + return points.Push(point) +} diff --git a/pkg/geo/geomfn/unary_operators_test.go b/pkg/geo/geomfn/unary_operators_test.go index 3ea8549f7c62..b9e49b807fa3 100644 --- a/pkg/geo/geomfn/unary_operators_test.go +++ b/pkg/geo/geomfn/unary_operators_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geopb" "github.com/stretchr/testify/require" ) @@ -123,3 +124,60 @@ func TestDimension(t *testing.T) { }) } } + +func TestPoints(t *testing.T) { + testCases := []struct { + wkt string + expected string + }{ + {"POINT EMPTY", "MULTIPOINT EMPTY"}, + {"POINT (1 2)", "MULTIPOINT (1 2)"}, + {"MULTIPOINT EMPTY", "MULTIPOINT EMPTY"}, + {"MULTIPOINT (1 2, 3 4)", "MULTIPOINT (1 2, 3 4)"}, + {"LINESTRING EMPTY", "MULTIPOINT EMPTY"}, + {"LINESTRING (1 2, 3 4)", "MULTIPOINT (1 2, 3 4)"}, + {"MULTILINESTRING EMPTY", "MULTIPOINT EMPTY"}, + {"MULTILINESTRING ((1 2, 3 4), (5 6, 7 8))", "MULTIPOINT (1 2, 3 4, 5 6, 7 8)"}, + {"POLYGON EMPTY", "MULTIPOINT EMPTY"}, + {"POLYGON ((1 2, 3 4, 5 6, 1 2))", "MULTIPOINT (1 2, 3 4, 5 6, 1 2)"}, + {"MULTIPOLYGON EMPTY", "MULTIPOINT EMPTY"}, + {"MULTIPOLYGON (((1 2, 3 4, 5 6, 1 2)), ((7 8, 9 0, 1 2, 7 8)))", "MULTIPOINT (1 2, 3 4, 5 6, 1 2, 7 8, 9 0, 1 2, 7 8)"}, + {"GEOMETRYCOLLECTION EMPTY", "MULTIPOINT EMPTY"}, + {"GEOMETRYCOLLECTION (GEOMETRYCOLLECTION EMPTY)", "MULTIPOINT EMPTY"}, + { + `GEOMETRYCOLLECTION ( + LINESTRING(1 1, 2 2), + POINT(1 1), + GEOMETRYCOLLECTION( + MULTIPOINT(2 2, 3 3), + GEOMETRYCOLLECTION EMPTY, + POINT(1 1), + MULTIPOLYGON(((1 2, 2 3, 3 4, 1 2))), + LINESTRING(3 3, 4 4), + GEOMETRYCOLLECTION( + POINT(4 4), + MULTIPOLYGON (EMPTY, ((1 2, 3 4, 5 6, 1 2), (2 3, 3 4, 4 5, 2 3)), EMPTY), + POINT(5 5) + ) + ), + MULTIPOINT EMPTY + )`, + "MULTIPOINT (1 1, 2 2, 1 1, 2 2, 3 3, 1 1, 1 2, 2 3, 3 4, 1 2, 3 3, 4 4, 4 4, 1 2, 3 4, 5 6, 1 2, 2 3, 3 4, 4 5, 2 3, 5 5)", + }, + } + + 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 := Points(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()) + }) + } +} diff --git a/pkg/sql/logictest/testdata/logic_test/geospatial b/pkg/sql/logictest/testdata/logic_test/geospatial index be49bb8d26eb..6528d1c0820e 100644 --- a/pkg/sql/logictest/testdata/logic_test/geospatial +++ b/pkg/sql/logictest/testdata/logic_test/geospatial @@ -2632,6 +2632,26 @@ Square (left) Polygon ST_Polygon Square (right) Polygon ST_Polygon 2 2 2 5 1 Square overlapping left and right square Polygon ST_Polygon 2 2 2 5 1 +query TT +SELECT + dsc, + ST_AsEWKT(ST_Points(geom)) +FROM geom_operators_test +ORDER BY dsc +---- +Empty GeometryCollection MULTIPOINT EMPTY +Empty LineString MULTIPOINT EMPTY +Empty Point MULTIPOINT EMPTY +Faraway point MULTIPOINT (5 5) +Line going through left and right square MULTIPOINT (-0.5 0.5, 0.5 0.5) +NULL NULL +Nested Geometry Collection MULTIPOINT (0 0) +Point middle of Left Square MULTIPOINT (-0.5 0.5) +Point middle of Right Square MULTIPOINT (0.5 0.5) +Square (left) MULTIPOINT (-1 0, 0 0, 0 1, -1 1, -1 0) +Square (right) MULTIPOINT (0 0, 1 0, 1 1, 0 1, 0 0) +Square overlapping left and right square MULTIPOINT (-0.1 0, 1 0, 1 1, -0.1 1, -0.1 0) + query TTTT SELECT a.dsc, diff --git a/pkg/sql/sem/builtins/geo_builtins.go b/pkg/sql/sem/builtins/geo_builtins.go index 05295a19075e..db58f8865b57 100644 --- a/pkg/sql/sem/builtins/geo_builtins.go +++ b/pkg/sql/sem/builtins/geo_builtins.go @@ -1775,6 +1775,23 @@ Flags shown square brackets after the geometry type have the following meaning: tree.VolatilityImmutable, ), ), + "st_points": makeBuiltin( + defProps(), + geometryOverload1( + func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) { + points, err := geomfn.Points(g.Geometry) + if err != nil { + return nil, err + } + return tree.NewDGeometry(points), nil + }, + types.Geometry, + infoBuilder{ + info: "Returns all coordinates in the given Geometry as a MultiPoint, including duplicates.", + }, + tree.VolatilityImmutable, + ), + ), "st_exteriorring": makeBuiltin( defProps(), geometryOverload1(