From dc09e28cd9f1b0a40ddc6bb331c7e89b10b8090e Mon Sep 17 00:00:00 2001 From: Oliver Tan Date: Thu, 16 Jul 2020 08:31:47 -0700 Subject: [PATCH] geomfn: implement validity operators Unfortunately we cannot implement ST_IsValidDetail because it returns a composite type, which we do not yet support. Release note (sql change): Implements ST_IsValid, ST_IsValidReason and ST_MakeValid operators for geometry types. --- docs/generated/sql/functions.md | 19 +++ pkg/geo/geomfn/validity_check.go | 74 +++++++++ pkg/geo/geomfn/validity_check_test.go | 148 ++++++++++++++++++ pkg/geo/geos/geos.cc | 111 +++++++++++++ pkg/geo/geos/geos.go | 79 ++++++++++ pkg/geo/geos/geos.h | 10 ++ .../logictest/testdata/logic_test/geospatial | 21 +++ pkg/sql/sem/builtins/geo_builtins.go | 119 +++++++++++++- 8 files changed, 579 insertions(+), 2 deletions(-) create mode 100644 pkg/geo/geomfn/validity_check.go create mode 100644 pkg/geo/geomfn/validity_check_test.go diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 9185f0867661..3d1286c6fa28 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -1097,6 +1097,22 @@ given Geometry.

This function utilizes the GEOS module.

This function variant will attempt to utilize any available geospatial index.

+st_isvalid(geometry: geometry) → bool

Returns whether the geometry is valid as defined by the OGC spec.

+

This function utilizes the GEOS module.

+
+st_isvalid(geometry: geometry, flags: int) → bool

Returns whether the geometry is valid.

+

For flags=0, validity is defined by the OGC spec.

+

For flags=1, validity considers self-intersecting rings forming holes as valid as per ESRI. This is not valid under OGC and CRDB spatial operations may not operate correctly.

+

This function utilizes the GEOS module.

+
+st_isvalidreason(geometry: geometry) → string

Returns a string containing the reason the geometry is invalid along with the point of interest, or “Valid Geometry” if it is valid. Validity is defined by the OGC spec.

+

This function utilizes the GEOS module.

+
+st_isvalidreason(geometry: geometry, flags: int) → string

Returns the reason the geometry is invalid or “Valid Geometry” if it is valid.

+

For flags=0, validity is defined by the OGC spec.

+

For flags=1, validity considers self-intersecting rings forming holes as valid as per ESRI. This is not valid under OGC and CRDB spatial operations may not operate correctly.

+

This function utilizes the GEOS module.

+
st_length(geography: geography) → float

Returns the length of the given geography in meters. Uses a spheroid to perform the operation.

This function utilizes the GeographicLib library for spheroid calculations.

@@ -1144,6 +1160,9 @@ given Geometry.

st_makepolygon(outer: geometry, interior: anyelement[]) → geometry

Returns a new Polygon with the given outer LineString and interior (hole) LineString(s).

+st_makevalid(geometry: geometry) → geometry

Returns a valid form of the given geometry.

+

This function utilizes the GEOS module.

+
st_maxdistance(geometry_a: geometry, geometry_b: geometry) → float

Returns the maximum distance across every pair of points comprising the given geometries. Note if the geometries are the same, it will return the maximum distance between the geometry’s vertexes.

st_mlinefromtext(str: string, srid: int) → geometry

Returns the Geometry from a WKT or EWKT representation with an SRID. If the shape underneath is not MultiLineString, NULL is returned. If the SRID is present in both the EWKT and the argument, the argument value is used.

diff --git a/pkg/geo/geomfn/validity_check.go b/pkg/geo/geomfn/validity_check.go new file mode 100644 index 000000000000..32716f090ab9 --- /dev/null +++ b/pkg/geo/geomfn/validity_check.go @@ -0,0 +1,74 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package geomfn + +import ( + "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/cockroach/pkg/geo/geos" +) + +// ValidDetail contains information about the validity of a geometry. +type ValidDetail struct { + IsValid bool + // Reason is only populated if IsValid = true. + Reason string + // InvalidLocation is only populated if IsValid = true. + InvalidLocation *geo.Geometry +} + +// IsValid returns whether the given Geometry is valid. +func IsValid(g *geo.Geometry) (bool, error) { + isValid, err := geos.IsValid(g.EWKB()) + if err != nil { + return false, err + } + return isValid, nil +} + +// IsValidReason returns the reasoning for whether the Geometry is valid or invalid. +func IsValidReason(g *geo.Geometry) (string, error) { + reason, err := geos.IsValidReason(g.EWKB()) + if err != nil { + return "", err + } + return reason, nil +} + +// IsValidDetail returns information about the validity of a Geometry. +// It takes in a flag parameter which behaves the same as the GEOS module, where 1 +// means that self-intersecting rings forming holes are considered valid. +func IsValidDetail(g *geo.Geometry, flags int) (ValidDetail, error) { + isValid, reason, locEWKB, err := geos.IsValidDetail(g.EWKB(), flags) + if err != nil { + return ValidDetail{}, err + } + var loc *geo.Geometry + if len(locEWKB) > 0 { + loc, err = geo.ParseGeometryFromEWKB(locEWKB) + if err != nil { + return ValidDetail{}, err + } + } + return ValidDetail{ + IsValid: isValid, + Reason: reason, + InvalidLocation: loc, + }, nil +} + +// MakeValid returns a valid form of the given Geometry. +func MakeValid(g *geo.Geometry) (*geo.Geometry, error) { + validEWKB, err := geos.MakeValid(g.EWKB()) + if err != nil { + return nil, err + } + return geo.ParseGeometryFromEWKB(validEWKB) +} diff --git a/pkg/geo/geomfn/validity_check_test.go b/pkg/geo/geomfn/validity_check_test.go new file mode 100644 index 000000000000..3420b176f023 --- /dev/null +++ b/pkg/geo/geomfn/validity_check_test.go @@ -0,0 +1,148 @@ +// Copyright 2020 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package geomfn + +import ( + "fmt" + "testing" + + "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/stretchr/testify/require" +) + +func TestIsValid(t *testing.T) { + testCases := []struct { + wkt string + expected bool + }{ + {"POINT(1.0 1.0)", true}, + {"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", true}, + {"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", true}, + + {"POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", false}, + } + + for _, tc := range testCases { + t.Run(tc.wkt, func(t *testing.T) { + g, err := geo.ParseGeometry(tc.wkt) + require.NoError(t, err) + ret, err := IsValid(g) + require.NoError(t, err) + require.Equal(t, tc.expected, ret) + }) + } +} + +func TestIsValidReason(t *testing.T) { + testCases := []struct { + wkt string + expected string + }{ + {"POINT(1.0 1.0)", "Valid Geometry"}, + {"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "Valid Geometry"}, + {"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "Valid Geometry"}, + + {"POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", "Self-intersection[1.5 1.5]"}, + } + + for _, tc := range testCases { + t.Run(tc.wkt, func(t *testing.T) { + g, err := geo.ParseGeometry(tc.wkt) + require.NoError(t, err) + ret, err := IsValidReason(g) + require.NoError(t, err) + require.Equal(t, tc.expected, ret) + }) + } +} + +func TestIsValidDetail(t *testing.T) { + testCases := []struct { + wkt string + flags int + expected ValidDetail + }{ + {"POINT(1.0 1.0)", 0, ValidDetail{IsValid: true}}, + {"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", 0, ValidDetail{IsValid: true}}, + {"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", 0, ValidDetail{IsValid: true}}, + {"POINT(1.0 1.0)", 1, ValidDetail{IsValid: true}}, + {"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", 1, ValidDetail{IsValid: true}}, + {"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", 1, ValidDetail{IsValid: true}}, + { + "POLYGON ((14 20, 8 45, 20 35, 14 20, 16 30, 12 30, 14 20))", + 1, + ValidDetail{IsValid: true}, + }, + + { + "POLYGON ((14 20, 8 45, 20 35, 14 20, 16 30, 12 30, 14 20))", + 0, + ValidDetail{ + IsValid: false, + Reason: "Ring Self-intersection", + InvalidLocation: geo.MustParseGeometry("POINT(14 20)"), + }, + }, + { + "POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", + 0, + ValidDetail{ + IsValid: false, + Reason: "Self-intersection", + InvalidLocation: geo.MustParseGeometry("POINT(1.5 1.5)"), + }, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s(%d)", tc.wkt, tc.flags), func(t *testing.T) { + g, err := geo.ParseGeometry(tc.wkt) + require.NoError(t, err) + ret, err := IsValidDetail(g, tc.flags) + require.NoError(t, err) + require.Equal(t, tc.expected, ret) + }) + } +} + +func TestMakeValid(t *testing.T) { + testCases := []struct { + wkt string + expected string + }{ + {"POINT(1.0 1.0)", "POINT(1.0 1.0)"}, + {"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)"}, + {"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))"}, + + { + "POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", + "GEOMETRYCOLLECTION(POLYGON((1 1,1.5 1.5,1.5 -1.5,1 1)),LINESTRING(1.5 1.5,2 2))", + }, + { + "SRID=4326;POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", + "SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1,1.5 1.5,1.5 -1.5,1 1)),LINESTRING(1.5 1.5,2 2))", + }, + } + + for _, tc := range testCases { + t.Run(tc.wkt, func(t *testing.T) { + g, err := geo.ParseGeometry(tc.wkt) + require.NoError(t, err) + + ret, err := MakeValid(g) + require.NoError(t, err) + + expected, err := geo.ParseGeometry(tc.expected) + require.NoError(t, err) + require.Equal(t, expected, ret) + }) + } +} diff --git a/pkg/geo/geos/geos.cc b/pkg/geo/geos/geos.cc index 9180e1399fa6..f2313ddecab6 100644 --- a/pkg/geo/geos/geos.cc +++ b/pkg/geo/geos/geos.cc @@ -83,6 +83,12 @@ typedef int (*CR_GEOS_BufferParams_setSingleSided_r)(CR_GEOS_Handle, CR_GEOS_Buf typedef CR_GEOS_Geometry (*CR_GEOS_BufferWithParams_r)(CR_GEOS_Handle, CR_GEOS_Geometry, CR_GEOS_BufferParams, double width); +typedef char (*CR_GEOS_isValid_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef char* (*CR_GEOS_isValidReason_r)(CR_GEOS_Handle, CR_GEOS_Geometry); +typedef char (*CR_GEOS_isValidDetail_r)(CR_GEOS_Handle, CR_GEOS_Geometry, int flags, char** reason, + CR_GEOS_Geometry* loc); +typedef CR_GEOS_Geometry (*CR_GEOS_MakeValid_r)(CR_GEOS_Handle, CR_GEOS_Geometry); + typedef int (*CR_GEOS_Area_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*); typedef int (*CR_GEOS_Length_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*); @@ -158,6 +164,11 @@ struct CR_GEOS { CR_GEOS_WKBReader_destroy_r GEOSWKBReader_destroy_r; CR_GEOS_WKBReader_read_r GEOSWKBReader_read_r; + CR_GEOS_isValid_r GEOSisValid_r; + CR_GEOS_isValidReason_r GEOSisValidReason_r; + CR_GEOS_isValidDetail_r GEOSisValidDetail_r; + CR_GEOS_MakeValid_r GEOSMakeValid_r; + CR_GEOS_Area_r GEOSArea_r; CR_GEOS_Length_r GEOSLength_r; @@ -229,6 +240,10 @@ struct CR_GEOS { INIT(GEOSGeomTypeId_r); INIT(GEOSSetSRID_r); INIT(GEOSGetSRID_r); + INIT(GEOSisValid_r); + INIT(GEOSisValidReason_r); + INIT(GEOSisValidDetail_r); + INIT(GEOSMakeValid_r); INIT(GEOSArea_r); INIT(GEOSLength_r); INIT(GEOSGetCentroid_r); @@ -513,6 +528,102 @@ CR_GEOS_Status CR_GEOS_Length(CR_GEOS* lib, CR_GEOS_Slice a, double* ret) { return CR_GEOS_UnaryOperator(lib, lib->GEOSLength_r, a, ret); } +// +// Validity checking. +// + +CR_GEOS_Status CR_GEOS_IsValid(CR_GEOS* lib, CR_GEOS_Slice g, char* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *ret = 0; + if (geom != nullptr) { + auto r = lib->GEOSisValid_r(handle, geom); + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *ret = r; + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_IsValidReason(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *ret = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto r = lib->GEOSisValidReason_r(handle, geom); + if (r != NULL) { + *ret = toGEOSString(r, strlen(r)); + lib->GEOSFree_r(handle, r); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_IsValidDetail(CR_GEOS* lib, CR_GEOS_Slice g, int flags, char* retIsValid, + CR_GEOS_String* retReason, CR_GEOS_String* retLocationEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *retReason = {.data = NULL, .len = 0}; + *retLocationEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + char* reason = NULL; + CR_GEOS_Geometry loc = NULL; + auto r = lib->GEOSisValidDetail_r(handle, geom, flags, &reason, &loc); + + if (r == 2) { + if (error.length() == 0) { + error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE); + } + } else { + *retIsValid = r; + } + + if (reason != NULL) { + *retReason = toGEOSString(reason, strlen(reason)); + lib->GEOSFree_r(handle, reason); + } + + if (loc != NULL) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, loc, retLocationEWKB, srid); + lib->GEOSGeom_destroy_r(handle, loc); + } + + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + +CR_GEOS_Status CR_GEOS_MakeValid(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* validEWKB) { + std::string error; + auto handle = initHandleWithErrorBuffer(lib, &error); + auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g); + *validEWKB = {.data = NULL, .len = 0}; + if (geom != nullptr) { + auto validGeom = lib->GEOSMakeValid_r(handle, geom); + if (validGeom != nullptr) { + auto srid = lib->GEOSGetSRID_r(handle, geom); + CR_GEOS_writeGeomToEWKB(lib, handle, validGeom, validEWKB, srid); + lib->GEOSGeom_destroy_r(handle, validGeom); + } + lib->GEOSGeom_destroy_r(handle, geom); + } + lib->GEOS_finish_r(handle); + return toGEOSString(error.data(), error.length()); +} + // // Topology operators. // diff --git a/pkg/geo/geos/geos.go b/pkg/geo/geos/geos.go index 1327e34e1789..d1da0f711750 100644 --- a/pkg/geo/geos/geos.go +++ b/pkg/geo/geos/geos.go @@ -580,3 +580,82 @@ func RelatePattern(a geopb.EWKB, b geopb.EWKB, pattern string) (bool, error) { } return ret == 1, nil } + +// +// Validity checking. +// + +// IsValid returns whether the given geometry is valid. +func IsValid(ewkb geopb.EWKB) (bool, error) { + g, err := ensureInitInternal() + if err != nil { + return false, err + } + var ret C.char + if err := statusToError( + C.CR_GEOS_IsValid(g, goToCSlice(ewkb), &ret), + ); err != nil { + return false, err + } + return ret == 1, nil +} + +// IsValidReason the reasoning for whether the Geometry is valid or invalid. +func IsValidReason(ewkb geopb.EWKB) (string, error) { + g, err := ensureInitInternal() + if err != nil { + return "", err + } + var ret C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_IsValidReason(g, goToCSlice(ewkb), &ret), + ); err != nil { + return "", err + } + + return string(cStringToSafeGoBytes(ret)), nil +} + +// IsValidDetail returns information regarding whether a geometry is valid or invalid. +// It takes in a flag parameter which behaves the same as the GEOS module, where 1 +// means that self-intersecting rings forming holes are considered valid. +// It returns a bool representing whether it is valid, a string giving a reason for +// invalidity, an EWKB representing the location things are invalid at. +func IsValidDetail(ewkb geopb.EWKB, flags int) (bool, string, geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return false, "", nil, err + } + var retIsValid C.char + var retReason C.CR_GEOS_String + var retLocationEWKB C.CR_GEOS_String + if err := statusToError( + C.CR_GEOS_IsValidDetail( + g, + goToCSlice(ewkb), + C.int(flags), + &retIsValid, + &retReason, + &retLocationEWKB, + ), + ); err != nil { + return false, "", nil, err + } + return retIsValid == 1, + string(cStringToSafeGoBytes(retReason)), + cStringToSafeGoBytes(retLocationEWKB), + nil +} + +// MakeValid returns a valid form of the EWKB. +func MakeValid(ewkb geopb.EWKB) (geopb.EWKB, error) { + g, err := ensureInitInternal() + if err != nil { + return nil, err + } + var cEWKB C.CR_GEOS_String + if err := statusToError(C.CR_GEOS_MakeValid(g, goToCSlice(ewkb), &cEWKB)); err != nil { + return nil, err + } + return cStringToSafeGoBytes(cEWKB), nil +} diff --git a/pkg/geo/geos/geos.h b/pkg/geo/geos/geos.h index 6cb9c11063b0..b387334ee853 100644 --- a/pkg/geo/geos/geos.h +++ b/pkg/geo/geos/geos.h @@ -69,6 +69,16 @@ CR_GEOS_Status CR_GEOS_ClipEWKBByRect(CR_GEOS* lib, CR_GEOS_Slice ewkb, double x CR_GEOS_Status CR_GEOS_Buffer(CR_GEOS* lib, CR_GEOS_Slice ewkb, CR_GEOS_BufferParamsInput params, double distance, CR_GEOS_String* ret); +// +// Validity checking. +// + +CR_GEOS_Status CR_GEOS_IsValid(CR_GEOS* lib, CR_GEOS_Slice g, char* ret); +CR_GEOS_Status CR_GEOS_IsValidReason(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret); +CR_GEOS_Status CR_GEOS_IsValidDetail(CR_GEOS* lib, CR_GEOS_Slice g, int flags, char* retIsValid, + CR_GEOS_String* retReason, CR_GEOS_String* retLocationEWKB); +CR_GEOS_Status CR_GEOS_MakeValid(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* validEWKB); + // // Unary operators. // diff --git a/pkg/sql/logictest/testdata/logic_test/geospatial b/pkg/sql/logictest/testdata/logic_test/geospatial index f939bc934bc7..78b183d1728d 100644 --- a/pkg/sql/logictest/testdata/logic_test/geospatial +++ b/pkg/sql/logictest/testdata/logic_test/geospatial @@ -580,6 +580,27 @@ Square (left) 1 Square (right) 1 Square overlapping left and right square 1.1 +# Validity checking. +query TBBBTTTT +SELECT + dsc, + ST_IsValid(geom), + ST_IsValid(geom, 0), + ST_IsValid(geom, 1), + ST_IsValidReason(geom), + ST_IsValidReason(geom, 0), + ST_IsValidReason(geom, 1), + ST_AsEWKT(ST_MakeValid(geom)) +FROM ( VALUES + ('valid geom', 'POINT(1.0 2.0)'::geometry), + ('invalid polygon', 'POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))'::geometry), + ('self-intersecting polygon', 'POLYGON ((14 20, 8 45, 20 35, 14 20, 16 30, 12 30, 14 20))'::geometry) +) t(dsc, geom) +---- +valid geom true true true Valid Geometry Valid Geometry Valid Geometry POINT (1 2) +invalid polygon false false false Self-intersection[1.5 1.5] Self-intersection Self-intersection GEOMETRYCOLLECTION (POLYGON ((1 1, 1.5 1.5, 1.5 -1.5, 1 1)), LINESTRING (1.5 1.5, 2 2)) +self-intersecting polygon false false true Ring Self-intersection[14 20] Ring Self-intersection Valid Geometry POLYGON ((14 20, 8 45, 20 35, 14 20), (14 20, 16 30, 12 30, 14 20)) + # Unary operators query TRRRR SELECT diff --git a/pkg/sql/sem/builtins/geo_builtins.go b/pkg/sql/sem/builtins/geo_builtins.go index a8c034d01f52..cb639fe1b3e5 100644 --- a/pkg/sql/sem/builtins/geo_builtins.go +++ b/pkg/sql/sem/builtins/geo_builtins.go @@ -2016,7 +2016,7 @@ The azimuth is angle is referenced from north, and is positive clockwise: North types.Geometry, infoBuilder{ info: `Returns the LineString corresponds to the max distance across every pair of points comprising the ` + - `given geometries. + `given geometries. Note if geometries are the same, it will return the LineString with the maximum distance between the geometry's ` + `vertexes. The function will return the longest line that was discovered first when comparing maximum ` + @@ -2041,7 +2041,7 @@ Note if geometries are the same, it will return the LineString with the maximum types.Geometry, infoBuilder{ info: `Returns the LineString corresponds to the minimum distance across every pair of points comprising ` + - `the given geometries. + `the given geometries. Note if geometries are the same, it will return the LineString with the minimum distance between the geometry's ` + `vertexes. The function will return the shortest line that was discovered first when comparing minimum ` + @@ -2332,7 +2332,122 @@ Note if geometries are the same, it will return the LineString with the minimum }, ), + // + // Validity checks + // + + "st_isvalid": makeBuiltin( + defProps(), + geometryOverload1( + func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) { + ret, err := geomfn.IsValid(g.Geometry) + if err != nil { + return nil, err + } + return tree.MakeDBool(tree.DBool(ret)), nil + }, + types.Bool, + infoBuilder{ + info: `Returns whether the geometry is valid as defined by the OGC spec.`, + libraryUsage: usesGEOS, + }, + tree.VolatilityImmutable, + ), + tree.Overload{ + Types: tree.ArgTypes{ + {"geometry", types.Geometry}, + {"flags", types.Int}, + }, + ReturnType: tree.FixedReturnType(types.Bool), + Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + g := args[0].(*tree.DGeometry) + flags := int(*args[1].(*tree.DInt)) + validDetail, err := geomfn.IsValidDetail(g.Geometry, flags) + if err != nil { + return nil, err + } + return tree.MakeDBool(tree.DBool(validDetail.IsValid)), nil + }, + Info: infoBuilder{ + info: `Returns whether the geometry is valid. + +For flags=0, validity is defined by the OGC spec. + +For flags=1, validity considers self-intersecting rings forming holes as valid as per ESRI. This is not valid under OGC and CRDB spatial operations may not operate correctly.`, + libraryUsage: usesGEOS, + }.String(), + Volatility: tree.VolatilityImmutable, + }, + ), + "st_isvalidreason": makeBuiltin( + defProps(), + geometryOverload1( + func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) { + ret, err := geomfn.IsValidReason(g.Geometry) + if err != nil { + return nil, err + } + return tree.NewDString(ret), nil + }, + types.String, + infoBuilder{ + info: `Returns a string containing the reason the geometry is invalid along with the point of interest, or "Valid Geometry" if it is valid. Validity is defined by the OGC spec.`, + libraryUsage: usesGEOS, + }, + tree.VolatilityImmutable, + ), + tree.Overload{ + Types: tree.ArgTypes{ + {"geometry", types.Geometry}, + {"flags", types.Int}, + }, + ReturnType: tree.FixedReturnType(types.String), + Fn: func(ctx *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + g := args[0].(*tree.DGeometry) + flags := int(*args[1].(*tree.DInt)) + validDetail, err := geomfn.IsValidDetail(g.Geometry, flags) + if err != nil { + return nil, err + } + if validDetail.IsValid { + return tree.NewDString("Valid Geometry"), nil + } + return tree.NewDString(validDetail.Reason), nil + }, + Info: infoBuilder{ + info: `Returns the reason the geometry is invalid or "Valid Geometry" if it is valid. + +For flags=0, validity is defined by the OGC spec. + +For flags=1, validity considers self-intersecting rings forming holes as valid as per ESRI. This is not valid under OGC and CRDB spatial operations may not operate correctly.`, + libraryUsage: usesGEOS, + }.String(), + Volatility: tree.VolatilityImmutable, + }, + ), + "st_makevalid": makeBuiltin( + defProps(), + geometryOverload1( + func(ctx *tree.EvalContext, g *tree.DGeometry) (tree.Datum, error) { + validGeom, err := geomfn.MakeValid(g.Geometry) + if err != nil { + return nil, err + } + return tree.NewDGeometry(validGeom), err + }, + types.Geometry, + infoBuilder{ + info: "Returns a valid form of the given geometry.", + libraryUsage: usesGEOS, + }, + tree.VolatilityImmutable, + ), + ), + + // // Topology operations + // + "st_centroid": makeBuiltin( defProps(), append(