diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index b6a692ed61fc..614e50a2f402 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -2203,6 +2203,10 @@ calculated, the result is transformed back into a Geography with SRID 4326.

st_makebox2d(geometry_a: geometry, geometry_b: geometry) → box2d

Creates a box2d from two points. Errors if arguments are not two non-empty points.

+st_makeenvelope(xmin: float, ymin: float, xmax: float, ymax: float) → geometry

Returns a box2d from bounds (the default srid value is 0).

+
+st_makeenvelope(xmin: float, ymin: float, xmax: float, ymax: float, srid: int) → geometry

Returns a box2d from bounds.

+
st_makepoint(x: float, y: float) → geometry

Returns a new Point with the given X and Y coordinates.

st_makepoint(x: float, y: float, z: float) → geometry

Returns a new Point with the given X, Y, and Z coordinates.

diff --git a/pkg/sql/sem/builtins/BUILD.bazel b/pkg/sql/sem/builtins/BUILD.bazel index e6085119943d..c0b62557f622 100644 --- a/pkg/sql/sem/builtins/BUILD.bazel +++ b/pkg/sql/sem/builtins/BUILD.bazel @@ -137,6 +137,7 @@ go_test( embed = [":builtins"], deps = [ "//pkg/base", + "//pkg/geo/geopb", "//pkg/keys", "//pkg/kv", "//pkg/security", diff --git a/pkg/sql/sem/builtins/geo_builtins.go b/pkg/sql/sem/builtins/geo_builtins.go index d1053efee2fc..a670d125bf13 100644 --- a/pkg/sql/sem/builtins/geo_builtins.go +++ b/pkg/sql/sem/builtins/geo_builtins.go @@ -5302,7 +5302,7 @@ The calculations are done on a sphere.`, }, Info: infoBuilder{ info: `Snaps the vertices and segments of input geometry the target geometry's vertices. -Tolerance is used to control where snapping is performed. The result geometry is the input geometry with the vertices snapped. +Tolerance is used to control where snapping is performed. The result geometry is the input geometry with the vertices snapped. If no snapping occurs then the input geometry is returned unchanged.`, }.String(), Volatility: volatility.Immutable, @@ -5558,6 +5558,42 @@ Bottom Left.`, Volatility: volatility.Immutable, }, ), + "st_makeenvelope": makeBuiltin( + defProps(), + tree.Overload{ + Types: tree.ArgTypes{ + {"xmin", types.Float}, + {"ymin", types.Float}, + {"xmax", types.Float}, + {"ymax", types.Float}, + {"srid", types.Int}, + }, + ReturnType: tree.FixedReturnType(types.Geometry), + Fn: func(_ *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + return makeGeometryFromBounds(args) + }, + Info: infoBuilder{ + info: "Returns a box2d from bounds.", + }.String(), + Volatility: tree.VolatilityImmutable, + }, + tree.Overload{ + Types: tree.ArgTypes{ + {"xmin", types.Float}, + {"ymin", types.Float}, + {"xmax", types.Float}, + {"ymax", types.Float}, + }, + ReturnType: tree.FixedReturnType(types.Geometry), + Fn: func(_ *tree.EvalContext, args tree.Datums) (tree.Datum, error) { + return makeGeometryFromBounds(args) + }, + Info: infoBuilder{ + info: "Returns a box2d from bounds (the default srid value is 0).", + }.String(), + Volatility: tree.VolatilityImmutable, + }, + ), "st_flipcoordinates": makeBuiltin( defProps(), tree.Overload{ @@ -6768,7 +6804,7 @@ May return a Point or LineString in the case of degenerate inputs.`, return tree.NewDInt(tree.DInt(ret)), nil }, Info: infoBuilder{ - info: `Returns an interger value defining behavior of crossing of lines: + info: `Returns an interger value defining behavior of crossing of lines: 0: lines do not cross, -1: linestring_b crosses linestring_a from right to left, 1: linestring_b crosses linestring_a from left to right, @@ -7502,3 +7538,28 @@ func applyGeoindexConfigStorageParams( } return indexDesc.GeoConfig, nil } + +// makeGeometryFromBounds builds a rectangle geometry from types.Float bounds as datums +func makeGeometryFromBounds(args tree.Datums) (tree.Datum, error) { + xmin := float64(tree.MustBeDFloat(args[0])) + ymin := float64(tree.MustBeDFloat(args[1])) + xmax := float64(tree.MustBeDFloat(args[2])) + ymax := float64(tree.MustBeDFloat(args[3])) + + var srid int + if len(args) > 4 { + srid = int(tree.MustBeDInt(args[4])) + } + + extent, err := geo.MakeGeometryFromGeomT( + geom.NewBounds(geom.XY). + Set(xmin, ymin, xmax, ymax). + Polygon(). + SetSRID(srid), + ) + if err != nil { + return nil, err + } + + return tree.NewDGeometry(extent), nil +} diff --git a/pkg/sql/sem/builtins/geo_builtins_test.go b/pkg/sql/sem/builtins/geo_builtins_test.go index a16c03bba7d1..96ff59d663a1 100644 --- a/pkg/sql/sem/builtins/geo_builtins_test.go +++ b/pkg/sql/sem/builtins/geo_builtins_test.go @@ -17,6 +17,7 @@ import ( "testing" "unicode" + "github.com/cockroachdb/cockroach/pkg/geo/geopb" "github.com/cockroachdb/cockroach/pkg/sql/randgen" "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" @@ -104,3 +105,88 @@ func TestGeoBuiltinsPointEmptyArgs(t *testing.T) { }) } } + +func mustParseDFloat(t testing.TB, in string) tree.Datum { + d, err := tree.ParseDFloat(in) + require.NoError(t, err) + + return d +} + +func mustParseDGeometry(t testing.TB, in string, srid int) tree.Datum { + d, err := tree.ParseDGeometry(in) + require.NoError(t, err) + + withSRID, err := d.CloneWithSRID(geopb.SRID(srid)) + require.NoError(t, err) + + return tree.NewDGeometry(withSRID) +} + +type multiArgsBuiltinFixture struct { + Title string + Builtin tree.Overload + Args tree.Datums + Expected tree.Datum + ExpectError bool +} + +func TestGeoBuiltinsSTMakeEnvelope(t *testing.T) { + defer leaktest.AfterTest(t)() + + // polygonGeometry, err := tree.ParseDGeometry("POLYGON((170 50,170 72,-130 72,-130 50,170 50))") + for _, toPin := range []multiArgsBuiltinFixture{ + { + Title: "st_makeenvelope happy path, srid", + Builtin: geoBuiltins["st_makeenvelope"].overloads[0], + Args: tree.Datums{ + mustParseDFloat(t, "30.01"), + mustParseDFloat(t, "50.01"), + mustParseDFloat(t, "72.01"), + mustParseDFloat(t, "52.01"), + tree.NewDInt(tree.DInt(4326)), + }, + Expected: mustParseDGeometry(t, "POLYGON((30.01 50.01,30.01 52.01,72.01 52.01,72.01 50.01,30.01 50.01))", 4326), + }, + { + Title: "st_makeenvelope happy path, unknown srid", + Builtin: geoBuiltins["st_makeenvelope"].overloads[1], + Args: tree.Datums{ + mustParseDFloat(t, "30.01"), + mustParseDFloat(t, "50.01"), + mustParseDFloat(t, "72.01"), + mustParseDFloat(t, "52.01"), + }, + Expected: mustParseDGeometry(t, "POLYGON((30.01 50.01,30.01 52.01,72.01 52.01,72.01 50.01,30.01 50.01))", 0), + }, + { + Title: "st_makeenvelope degenerate bounds, unknown srid", + Builtin: geoBuiltins["st_makeenvelope"].overloads[1], + Args: tree.Datums{ + mustParseDFloat(t, "30"), + mustParseDFloat(t, "50"), + mustParseDFloat(t, "30"), + mustParseDFloat(t, "50"), + }, + Expected: mustParseDGeometry(t, "POLYGON((30 50,30 50,30 50,30 50,30 50))", 0), + }, + } { + testcase := toPin + + t.Run(testcase.Title, func(t *testing.T) { + t.Parallel() // SAFE FOR TESTING + + overload := testcase.Builtin + datums := testcase.Args + res, err := overload.Fn(&tree.EvalContext{}, datums) + if testcase.ExpectError { + require.Error(t, err) + + return + } + + require.NoError(t, err) + require.EqualValues(t, testcase.Expected, res) + }) + } +}