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]

Reply via email to