This is an automated email from the ASF dual-hosted git repository.
morrySnow pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new c88e889515e [feature](function) Add ST_IsClosed function (#67350)
c88e889515e is described below
commit c88e889515ecc33e90a503c274bf58e76a5758a5
Author: YanzhiJin5 <[email protected]>
AuthorDate: Tue Sep 8 16:15:48 2026 +0800
[feature](function) Add ST_IsClosed function (#67350)
### What problem does this PR solve?
Issue Number: ref #48203
Related PR: apache/doris-website#4102
Problem Summary:
Add the `ST_IsClosed` spatial scalar function.
For a valid LineString, the function returns:
* `true` when the first and last points are exactly equal
* `false` when the LineString is open
* `NULL` for a `NULL` input
* `NULL` for a non-LineString or invalid encoded geometry
The implementation uses exact S2 point equality without introducing a
tolerance.
This PR also adds Nereids registration and visitor support, FE and BE
unit tests, and a self-asserting SQL regression suite.
### Release note
Add the `ST_IsClosed` spatial function.
---
be/src/exprs/function/geo/functions_geo.cpp | 41 ++++++++++++
be/test/exprs/function/geo/functions_geo_test.cpp | 32 +++++++++
.../doris/catalog/BuiltinScalarFunctions.java | 2 +
.../expressions/functions/scalar/StIsClosed.java | 75 ++++++++++++++++++++++
.../expressions/visitor/ScalarFunctionVisitor.java | 5 ++
.../scalar/StGeoComponentFunctionsTest.java | 31 ++++++++-
.../spatial_functions/test_st_isclosed.groovy | 30 +++++++++
7 files changed, 215 insertions(+), 1 deletion(-)
diff --git a/be/src/exprs/function/geo/functions_geo.cpp
b/be/src/exprs/function/geo/functions_geo.cpp
index b4967780d2c..00b2735ace2 100644
--- a/be/src/exprs/function/geo/functions_geo.cpp
+++ b/be/src/exprs/function/geo/functions_geo.cpp
@@ -18,6 +18,7 @@
#include "exprs/function/geo/functions_geo.h"
#include <glog/logging.h>
+#include <s2/s2point.h>
#include <algorithm>
#include <boost/iterator/iterator_facade.hpp>
@@ -780,6 +781,45 @@ struct StGeometryType {
}
};
+struct StIsClosed {
+ static constexpr auto NAME = "st_isclosed";
+ static const size_t NUM_ARGS = 1;
+ using Type = DataTypeUInt8;
+
+ static Status execute(Block& block, const ColumnNumbers& arguments, size_t
result) {
+ DCHECK_EQ(arguments.size(), 1);
+
+ auto col =
ColumnView<TYPE_STRING>::create(block.get_by_position(arguments[0]).column);
+ const auto size = col.size();
+
+ auto res = ColumnUInt8::create(size, 0);
+ auto null_map = ColumnUInt8::create(size, 0);
+ auto& result_data = res->get_data();
+ auto& null_map_data = null_map->get_data();
+
+ GeoLine line;
+ for (int row = 0; row < size; ++row) {
+ auto value = col.value_at(row);
+ if (!line.decode_from(value.data, value.size)) {
+ null_map_data[row] = 1;
+ continue;
+ }
+
+ const auto num_points = line.numPoint();
+ if (num_points < 2) {
+ null_map_data[row] = 1;
+ continue;
+ }
+
+ result_data[row] = *line.getPoint(0) == *line.getPoint(num_points
- 1);
+ }
+
+ block.replace_by_position(result,
+ ColumnNullable::create(std::move(res),
std::move(null_map)));
+ return Status::OK();
+ }
+};
+
struct StDistance {
static constexpr auto NAME = "st_distance";
static const size_t NUM_ARGS = 2;
@@ -1107,6 +1147,7 @@ void register_function_geo(SimpleFunctionFactory&
factory) {
factory.register_function<GeoFunction<StAsBinary>>();
factory.register_function<GeoFunction<StLength>>();
factory.register_function<GeoFunction<StGeometryType>>();
+ factory.register_function<GeoFunction<StIsClosed>>();
factory.register_function<GeoFunction<StDistance>>();
factory.register_function<GeoFunction<StNumGeometries>>();
factory.register_function<GeoFunction<StNumPoints>>();
diff --git a/be/test/exprs/function/geo/functions_geo_test.cpp
b/be/test/exprs/function/geo/functions_geo_test.cpp
index 338df011af0..0dce1c876ab 100644
--- a/be/test/exprs/function/geo/functions_geo_test.cpp
+++ b/be/test/exprs/function/geo/functions_geo_test.cpp
@@ -218,6 +218,38 @@ TEST(VGeoFunctionsTest, function_geo_st_numpoints_invalid)
{
static_cast<void>(check_function<DataTypeInt64, true>(func_name,
input_types, data_set));
}
+// ==================== ST_IsClosed Tests ====================
+
+TEST(VGeoFunctionsTest, function_geo_st_isclosed) {
+ std::string func_name = "st_isclosed";
+ InputTypeSet input_types = {PrimitiveType::TYPE_VARCHAR};
+
+ auto encode_wkt = [](const std::string& wkt) {
+ GeoParseStatus status;
+ auto shape = GeoShape::from_wkt(wkt.data(), wkt.size(), status);
+ EXPECT_EQ(status, GEO_PARSE_OK);
+ EXPECT_NE(shape, nullptr);
+
+ std::string buf;
+ shape->encode_to(&buf);
+ return buf;
+ };
+
+ auto closed_line = encode_wkt("LINESTRING (0 0, 1 1, 0 0)");
+ auto open_line = encode_wkt("LINESTRING (0 0, 1 1, 2 2)");
+ auto nearly_closed_line = encode_wkt("LINESTRING (0 0, 1 1, 0
0.0000000000001)");
+ auto point = encode_wkt("POINT (0 0)");
+
+ DataSet data_set = {{{closed_line}, uint8_t(1)},
+ {{open_line}, uint8_t(0)},
+ {{nearly_closed_line}, uint8_t(0)},
+ {{point}, Null()},
+ {{std::string("invalid_geometry_data")}, Null()},
+ {{Null()}, Null()}};
+
+ check_function_all_arg_comb<DataTypeUInt8, true>(func_name, input_types,
data_set);
+}
+
// ==================== ST_Geometries Tests ====================
TEST(VGeoFunctionsTest, function_geo_st_geometries_point) {
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
index ba1c1ab4b28..af2826d7b11 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/catalog/BuiltinScalarFunctions.java
@@ -501,6 +501,7 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.StGeometryTyp
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StGeometryfromtext;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StGeomfromtext;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StIntersects;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.StIsClosed;
import org.apache.doris.nereids.trees.expressions.functions.scalar.StLength;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StLinefromtext;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StLinestringfromtext;
@@ -1096,6 +1097,7 @@ public class BuiltinScalarFunctions implements
FunctionHelper {
scalar(StTouches.class, "st_touches"),
scalar(StLength.class, "st_length"),
scalar(StGeometryType.class, "st_geometrytype"),
+ scalar(StIsClosed.class, "st_isclosed"),
scalar(StNumGeometries.class, "st_numgeometries"),
scalar(StGeometries.class, "st_geometries"),
scalar(StNumPoints.class, "st_numpoints", "st_npoints"),
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StIsClosed.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StIsClosed.java
new file mode 100644
index 00000000000..10802f77c11
--- /dev/null
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StIsClosed.java
@@ -0,0 +1,75 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.functions.AlwaysNullable;
+import
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
+import
org.apache.doris.nereids.trees.expressions.functions.PropagateNullLiteral;
+import org.apache.doris.nereids.trees.expressions.shape.UnaryExpression;
+import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
+import org.apache.doris.nereids.types.BooleanType;
+import org.apache.doris.nereids.types.VarcharType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+/**
+ * ScalarFunction 'st_isclosed'.
+ */
+public class StIsClosed extends ScalarFunction
+ implements UnaryExpression, ExplicitlyCastableSignature,
AlwaysNullable, PropagateNullLiteral {
+
+ public static final List<FunctionSignature> SIGNATURES = ImmutableList.of(
+
FunctionSignature.ret(BooleanType.INSTANCE).args(VarcharType.SYSTEM_DEFAULT)
+ );
+
+ /**
+ * constructor with 1 argument.
+ */
+ public StIsClosed(Expression arg0) {
+ super("st_isclosed", arg0);
+ }
+
+ /** constructor for withChildren and reuse signature */
+ private StIsClosed(ScalarFunctionParams functionParams) {
+ super(functionParams);
+ }
+
+ /**
+ * withChildren.
+ */
+ @Override
+ public StIsClosed withChildren(List<Expression> children) {
+ Preconditions.checkArgument(children.size() == 1);
+ return new StIsClosed(getFunctionParams(children));
+ }
+
+ @Override
+ public List<FunctionSignature> getSignatures() {
+ return SIGNATURES;
+ }
+
+ @Override
+ public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
+ return visitor.visitStIsClosed(this, context);
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
index 9c64d2bcf05..26930ccd27a 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/visitor/ScalarFunctionVisitor.java
@@ -519,6 +519,7 @@ import
org.apache.doris.nereids.trees.expressions.functions.scalar.StGeometryTyp
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StGeometryfromtext;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StGeomfromtext;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StIntersects;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.StIsClosed;
import org.apache.doris.nereids.trees.expressions.functions.scalar.StLength;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StLinefromtext;
import
org.apache.doris.nereids.trees.expressions.functions.scalar.StLinestringfromtext;
@@ -2429,6 +2430,10 @@ public interface ScalarFunctionVisitor<R, C> {
return visitScalarFunction(stGeometryType, context);
}
+ default R visitStIsClosed(StIsClosed stIsClosed, C context) {
+ return visitScalarFunction(stIsClosed, context);
+ }
+
default R visitStNumGeometries(StNumGeometries stNumGeometries, C context)
{
return visitScalarFunction(stNumGeometries, context);
}
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StGeoComponentFunctionsTest.java
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StGeoComponentFunctionsTest.java
index a976e15aeb7..583b96748a1 100644
---
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StGeoComponentFunctionsTest.java
+++
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/scalar/StGeoComponentFunctionsTest.java
@@ -22,6 +22,7 @@ import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
import org.apache.doris.nereids.types.ArrayType;
import org.apache.doris.nereids.types.BigIntType;
+import org.apache.doris.nereids.types.BooleanType;
import org.apache.doris.nereids.types.VarcharType;
import com.google.common.collect.ImmutableList;
@@ -31,7 +32,7 @@ import org.junit.jupiter.api.Test;
import java.util.List;
/**
- * Unit tests for ST_NumGeometries, ST_NumPoints, and ST_Geometries scalar
functions.
+ * Unit tests for ST_NumGeometries, ST_NumPoints, ST_Geometries, and
ST_IsClosed scalar functions.
*/
public class StGeoComponentFunctionsTest {
@@ -133,4 +134,32 @@ public class StGeoComponentFunctionsTest {
Assertions.assertTrue(arrayType.getItemType() instanceof VarcharType);
}
+ @Test
+ public void testStIsClosedBasicProperties() {
+ Expression arg = new VarcharLiteral("test");
+ StIsClosed func = new StIsClosed(arg);
+
+ Assertions.assertEquals("st_isclosed", func.getName());
+ Assertions.assertEquals(1, func.arity());
+ Assertions.assertTrue(func.nullable());
+
+ List<FunctionSignature> signatures = func.getSignatures();
+ Assertions.assertEquals(1, signatures.size());
+ Assertions.assertEquals(BooleanType.INSTANCE,
signatures.get(0).returnType);
+ Assertions.assertEquals(VarcharType.SYSTEM_DEFAULT,
signatures.get(0).getArgType(0));
+ }
+
+ @Test
+ public void testStIsClosedWithChildren() {
+ Expression arg = new VarcharLiteral("test");
+ StIsClosed func = new StIsClosed(arg);
+
+ Expression newArg = new VarcharLiteral("new_test");
+ StIsClosed newFunc = func.withChildren(ImmutableList.of(newArg));
+
+ Assertions.assertNotSame(func, newFunc);
+ Assertions.assertEquals("st_isclosed", newFunc.getName());
+ Assertions.assertEquals(1, newFunc.arity());
+ }
+
}
diff --git
a/regression-test/suites/query_p0/sql_functions/spatial_functions/test_st_isclosed.groovy
b/regression-test/suites/query_p0/sql_functions/spatial_functions/test_st_isclosed.groovy
new file mode 100644
index 00000000000..b786df1e261
--- /dev/null
+++
b/regression-test/suites/query_p0/sql_functions/spatial_functions/test_st_isclosed.groovy
@@ -0,0 +1,30 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_st_isclosed") {
+ def result = sql """
+ SELECT ST_IsClosed(ST_LineFromText('LINESTRING (0 0, 1 1, 0 0)')),
+ ST_IsClosed(ST_LineFromText('LINESTRING (0 0, 1 1, 2 2)')),
+ ST_IsClosed(NULL),
+ ST_IsClosed(ST_Point(0, 0))
+ """
+
+ assertEquals(true, result[0][0])
+ assertEquals(false, result[0][1])
+ assertEquals(null, result[0][2])
+ assertEquals(null, result[0][3])
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]