This is an automated email from the ASF dual-hosted git repository.

zclllyybb 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 c2432387467 [Enhancement](udf) support volatility for udaf && udtf 
(#63611)
c2432387467 is described below

commit c24323874675762d1b42b8487b3dcd9b5ed81061
Author: linrrarity <[email protected]>
AuthorDate: Mon Jun 1 14:51:34 2026 +0800

    [Enhancement](udf) support volatility for udaf && udtf (#63611)
    
    Related PR: https://github.com/apache/doris/pull/62698
    
    Problem Summary:
    
    Add volatility metadata support for UDAF and UDTF definitions, so
    user-defined aggregate and table functions can preserve and expose their
    volatility semantics consistently with UDFs.
---
 .../doris/catalog/FunctionToSqlConverter.java      | 10 ++--
 .../nereids/trees/expressions/functions/Udf.java   | 19 ++++++-
 .../trees/expressions/functions/udf/JavaUdaf.java  | 58 ++++++++++++++++++++-
 .../expressions/functions/udf/JavaUdafBuilder.java |  2 +-
 .../trees/expressions/functions/udf/JavaUdf.java   | 17 +++----
 .../trees/expressions/functions/udf/JavaUdtf.java  | 58 ++++++++++++++++++++-
 .../expressions/functions/udf/JavaUdtfBuilder.java |  2 +-
 .../expressions/functions/udf/PythonUdaf.java      | 59 +++++++++++++++++++++-
 .../functions/udf/PythonUdafBuilder.java           |  2 +-
 .../trees/expressions/functions/udf/PythonUdf.java | 17 +++----
 .../expressions/functions/udf/PythonUdtf.java      | 58 ++++++++++++++++++++-
 .../functions/udf/PythonUdtfBuilder.java           |  2 +-
 .../plans/commands/CreateFunctionCommand.java      | 21 ++++----
 .../trees/plans/commands/ShowFunctionsCommand.java |  9 ++--
 .../apache/doris/catalog/CreateFunctionTest.java   | 11 ++++
 .../doris/catalog/FunctionToSqlConverterTest.java  | 12 +++--
 .../functions/udf/UdfVolatilityTest.java           | 52 +++++++++++++++++++
 .../plans/commands/ShowFunctionsCommandTest.java   | 33 +++++++++---
 18 files changed, 379 insertions(+), 63 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/FunctionToSqlConverter.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/FunctionToSqlConverter.java
index 49db8ed7e24..590a6582594 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/FunctionToSqlConverter.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/FunctionToSqlConverter.java
@@ -78,18 +78,14 @@ public class FunctionToSqlConverter {
                     .append("\"" + (fn.getLocation() == null ? "" : 
fn.getLocation().toString()) + "\"");
             boolean isReturnNull = fn.getNullableMode() == 
NullableMode.ALWAYS_NULLABLE;
             sb.append(",\n  \"ALWAYS_NULLABLE\"=").append("\"" + isReturnNull 
+ "\"");
-            if (!fn.isUDTFunction()) {
-                sb.append(",\n  \"VOLATILITY\"=").append("\"" + 
fn.getVolatility().toSql() + "\"");
-            }
+            sb.append(",\n  \"VOLATILITY\"=").append("\"" + 
fn.getVolatility().toSql() + "\"");
         } else if (fn.getBinaryType() == Function.BinaryType.PYTHON_UDF) {
             appendFileIfPresent(sb, fn, true);
             boolean isReturnNull = fn.getNullableMode() == 
NullableMode.ALWAYS_NULLABLE;
             sb.append(",\n  \"ALWAYS_NULLABLE\"=").append("\"" + isReturnNull 
+ "\"");
             sb.append(",\n  \"RUNTIME_VERSION\"=").append("\"" + 
Strings.nullToEmpty(fn.getRuntimeVersion()) + "\"");
             appendExpirationTimeIfNeeded(sb, fn);
-            if (!fn.isUDTFunction()) {
-                sb.append(",\n  \"VOLATILITY\"=").append("\"" + 
fn.getVolatility().toSql() + "\"");
-            }
+            sb.append(",\n  \"VOLATILITY\"=").append("\"" + 
fn.getVolatility().toSql() + "\"");
         } else {
             sb.append(",\n  \"OBJECT_FILE\"=")
                     .append("\"" + (fn.getLocation() == null ? "" : 
fn.getLocation().toString()) + "\"");
@@ -167,6 +163,7 @@ public class FunctionToSqlConverter {
                     .append("\"" + (fn.getLocation() == null ? "" : 
fn.getLocation().toString()) + "\",");
             boolean isReturnNull = fn.getNullableMode() == 
NullableMode.ALWAYS_NULLABLE;
             sb.append("\n  \"ALWAYS_NULLABLE\"=").append("\"" + isReturnNull + 
"\",");
+            sb.append("\n  \"VOLATILITY\"=").append("\"" + 
fn.getVolatility().toSql() + "\",");
         } else if (fn.getBinaryType() == Function.BinaryType.PYTHON_UDF) {
             appendFileIfPresent(sb, fn, false);
             if (fn.getLocation() != null) {
@@ -179,6 +176,7 @@ public class FunctionToSqlConverter {
             if (fn.getExpirationTime() != DEFAULT_EXPIRATION_TIME) {
                 sb.append("\n  \"EXPIRATION_TIME\"=").append("\"" + 
fn.getExpirationTime() + "\",");
             }
+            sb.append("\n  \"VOLATILITY\"=").append("\"" + 
fn.getVolatility().toSql() + "\",");
         } else {
             sb.append("\n  \"OBJECT_FILE\"=")
                     .append("\"" + (fn.getLocation() == null ? "" : 
fn.getLocation().toString()) + "\",");
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Udf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Udf.java
index 7b4229058c1..dc66c259d99 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Udf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/Udf.java
@@ -19,15 +19,18 @@ package 
org.apache.doris.nereids.trees.expressions.functions;
 
 import org.apache.doris.catalog.Function;
 import org.apache.doris.catalog.Function.NullableMode;
+import org.apache.doris.catalog.FunctionVolatility;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.VolatileExpression;
+import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 
 import java.util.List;
 
 /**
  * interface for udf
  */
-public interface Udf extends ComputeNullable {
+public interface Udf extends ComputeNullable, VolatileExpression {
     @Override
     default boolean nullable() {
         NullableMode mode = getNullableMode();
@@ -45,8 +48,22 @@ public interface Udf extends ComputeNullable {
 
     NullableMode getNullableMode();
 
+    FunctionVolatility getVolatility();
+
     List<Expression> children();
 
+    @Override
+    default boolean isDeterministic() {
+        return getVolatility() == FunctionVolatility.IMMUTABLE;
+    }
+
+    Udf withFreshVolatileIdentity();
+
+    static VolatileIdentity createVolatileIdentity(FunctionVolatility 
volatility) {
+        return volatility == FunctionVolatility.VOLATILE
+                ? VolatileIdentity.newVolatileIdentity() : 
VolatileIdentity.NON_VOLATILE;
+    }
+
     @Override
     default boolean foldable() {
         // Udf should not be folded in FE.
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdaf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdaf.java
index c3eebfc283f..27fc9c98f10 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdaf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdaf.java
@@ -22,11 +22,13 @@ import org.apache.doris.catalog.Function;
 import org.apache.doris.catalog.Function.NullableMode;
 import org.apache.doris.catalog.FunctionName;
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.catalog.FunctionVolatility;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.util.URI;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.Udf;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
@@ -50,6 +52,8 @@ public class JavaUdaf extends AggregateFunction implements 
ExplicitlyCastableSig
     private final FunctionSignature signature;
     private final DataType intermediateType;
     private final NullableMode nullableMode;
+    private final FunctionVolatility volatility;
+    private final VolatileIdentity volatileIdentity;
     private final String objectFile;
     private final String symbol;
     private final String initFn;
@@ -69,7 +73,7 @@ public class JavaUdaf extends AggregateFunction implements 
ExplicitlyCastableSig
     public JavaUdaf(String name, long functionId, String dbName, 
Function.BinaryType binaryType,
             FunctionSignature signature,
             DataType intermediateType, NullableMode nullableMode,
-            String objectFile, String symbol,
+            FunctionVolatility volatility, VolatileIdentity volatileIdentity, 
String objectFile, String symbol,
             String initFn, String updateFn, String mergeFn,
             String serializeFn, String finalizeFn, String getValueFn, String 
removeFn,
             boolean isDistinct, String checkSum, boolean isStaticLoad, long 
expirationTime, Expression... args) {
@@ -80,6 +84,8 @@ public class JavaUdaf extends AggregateFunction implements 
ExplicitlyCastableSig
         this.signature = signature;
         this.intermediateType = intermediateType == null ? 
signature.returnType : intermediateType;
         this.nullableMode = nullableMode;
+        this.volatility = volatility;
+        this.volatileIdentity = volatileIdentity;
         this.objectFile = objectFile;
         this.symbol = symbol;
         this.initFn = initFn;
@@ -121,8 +127,48 @@ public class JavaUdaf extends AggregateFunction implements 
ExplicitlyCastableSig
     public JavaUdaf withDistinctAndChildren(boolean isDistinct, 
List<Expression> children) {
         Preconditions.checkArgument(children.size() == this.children.size());
         return new JavaUdaf(getName(), functionId, dbName, binaryType, 
signature, intermediateType, nullableMode,
+                volatility, volatileIdentity, objectFile, symbol, initFn, 
updateFn, mergeFn, serializeFn, finalizeFn,
+                getValueFn, removeFn, isDistinct, checkSum, isStaticLoad, 
expirationTime,
+                children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public VolatileIdentity getVolatileIdentity() {
+        return volatileIdentity;
+    }
+
+    @Override
+    public JavaUdaf withIgnoreUniqueId(boolean ignoreUniqueId) {
+        Preconditions.checkState(isVolatile(), "Only volatile Java UDAF can 
ignore unique id");
+        return new JavaUdaf(getName(), functionId, dbName, binaryType, 
signature, intermediateType, nullableMode,
+                volatility, 
volatileIdentity.withIgnoreUniqueId(ignoreUniqueId),
                 objectFile, symbol, initFn, updateFn, mergeFn, serializeFn, 
finalizeFn, getValueFn, removeFn,
-                isDistinct, checkSum, isStaticLoad, expirationTime, 
children.toArray(new Expression[0]));
+                distinct, checkSum, isStaticLoad, expirationTime, 
children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public JavaUdaf withFreshVolatileIdentity() {
+        if (volatility != FunctionVolatility.VOLATILE) {
+            return this;
+        }
+        return new JavaUdaf(getName(), functionId, dbName, binaryType, 
signature, intermediateType, nullableMode,
+                volatility, VolatileIdentity.newVolatileIdentity(),
+                objectFile, symbol, initFn, updateFn, mergeFn, serializeFn, 
finalizeFn, getValueFn, removeFn,
+                distinct, checkSum, isStaticLoad, expirationTime, 
children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof JavaUdaf)) {
+            return false;
+        }
+        JavaUdaf other = (JavaUdaf) o;
+        return volatileIdentity.equalsByIdentity(other.volatileIdentity, 
super.equals(o));
+    }
+
+    @Override
+    public int computeHashCode() {
+        return volatileIdentity.hashCodeByIdentity(super.computeHashCode());
     }
 
     /**
@@ -152,6 +198,8 @@ public class JavaUdaf extends AggregateFunction implements 
ExplicitlyCastableSig
         JavaUdaf udaf = new JavaUdaf(fnName, aggregate.getId(), dbName, 
aggregate.getBinaryType(), sig,
                 intermediateType,
                 aggregate.getNullableMode(),
+                aggregate.getVolatility(),
+                Udf.createVolatileIdentity(aggregate.getVolatility()),
                 aggregate.getLocation() == null ? null : 
aggregate.getLocation().getLocation(),
                 aggregate.getSymbolName(),
                 aggregate.getInitFnSymbol(),
@@ -201,9 +249,15 @@ public class JavaUdaf extends AggregateFunction implements 
ExplicitlyCastableSig
             expr.setId(functionId);
             expr.setStaticLoad(isStaticLoad);
             expr.setExpirationTime(expirationTime);
+            expr.setVolatility(volatility);
             return expr;
         } catch (Exception e) {
             throw new AnalysisException(e.getMessage(), e.getCause());
         }
     }
+
+    @Override
+    public FunctionVolatility getVolatility() {
+        return volatility;
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdafBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdafBuilder.java
index c88f24f4eec..4822ab6ef1a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdafBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdafBuilder.java
@@ -79,7 +79,7 @@ public class JavaUdafBuilder extends UdfBuilder {
 
     @Override
     public Pair<JavaUdaf, JavaUdaf> build(String name, List<?> arguments) {
-        return Pair.ofSame((JavaUdaf) udaf.withChildren(
+        return Pair.ofSame((JavaUdaf) 
udaf.withFreshVolatileIdentity().withChildren(
                 arguments.stream()
                         .map(Expression.class::cast)
                         .collect(Collectors.toList()))
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdf.java
index 3e53200fa9c..76e98ed39e5 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdf.java
@@ -28,7 +28,6 @@ import org.apache.doris.common.util.URI;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
-import org.apache.doris.nereids.trees.expressions.VolatileExpression;
 import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.Udf;
@@ -46,7 +45,7 @@ import java.util.stream.Collectors;
 /**
  * Java UDF for Nereids
  */
-public class JavaUdf extends ScalarFunction implements 
ExplicitlyCastableSignature, Udf, VolatileExpression {
+public class JavaUdf extends ScalarFunction implements 
ExplicitlyCastableSignature, Udf {
     private final String dbName;
     private final long functionId;
     private final Function.BinaryType binaryType;
@@ -134,6 +133,7 @@ public class JavaUdf extends ScalarFunction implements 
ExplicitlyCastableSignatu
     }
 
     /** Return a copy with a new per-call identity when this UDF is VOLATILE. 
*/
+    @Override
     public JavaUdf withFreshVolatileIdentity() {
         if (volatility != FunctionVolatility.VOLATILE) {
             return this;
@@ -144,11 +144,6 @@ public class JavaUdf extends ScalarFunction implements 
ExplicitlyCastableSignatu
                 children.toArray(new Expression[0]));
     }
 
-    @Override
-    public boolean isDeterministic() {
-        return volatility == FunctionVolatility.IMMUTABLE;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (!(o instanceof JavaUdf)) {
@@ -183,7 +178,7 @@ public class JavaUdf extends ScalarFunction implements 
ExplicitlyCastableSignatu
                 .toArray(SlotReference[]::new);
 
         JavaUdf udf = new JavaUdf(fnName, scalar.getId(), dbName, 
scalar.getBinaryType(), sig,
-                scalar.getNullableMode(), scalar.getVolatility(), 
createVolatileIdentity(scalar.getVolatility()),
+                scalar.getNullableMode(), scalar.getVolatility(), 
Udf.createVolatileIdentity(scalar.getVolatility()),
                 scalar.getLocation() == null ? null : 
scalar.getLocation().getLocation(),
                 scalar.getSymbolName(),
                 scalar.getPrepareFnSymbol(),
@@ -226,8 +221,8 @@ public class JavaUdf extends ScalarFunction implements 
ExplicitlyCastableSignatu
         }
     }
 
-    private static VolatileIdentity createVolatileIdentity(FunctionVolatility 
volatility) {
-        return volatility == FunctionVolatility.VOLATILE
-                ? VolatileIdentity.newVolatileIdentity() : 
VolatileIdentity.NON_VOLATILE;
+    @Override
+    public FunctionVolatility getVolatility() {
+        return volatility;
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtf.java
index 2e04dec1d68..401ab968392 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtf.java
@@ -22,11 +22,13 @@ import org.apache.doris.catalog.Function;
 import org.apache.doris.catalog.Function.NullableMode;
 import org.apache.doris.catalog.FunctionName;
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.catalog.FunctionVolatility;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.util.URI;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.Udf;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.TableGeneratingFunction;
@@ -49,6 +51,8 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
     private final Function.BinaryType binaryType;
     private final FunctionSignature signature;
     private final NullableMode nullableMode;
+    private final FunctionVolatility volatility;
+    private final VolatileIdentity volatileIdentity;
     private final String objectFile;
     private final String symbol;
     private final String prepareFn;
@@ -62,7 +66,9 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
      */
     public JavaUdtf(String name, long functionId, String dbName, 
Function.BinaryType binaryType,
             FunctionSignature signature,
-            NullableMode nullableMode, String objectFile, String symbol, 
String prepareFn, String closeFn,
+            NullableMode nullableMode, FunctionVolatility volatility, 
VolatileIdentity volatileIdentity,
+            String objectFile, String symbol,
+            String prepareFn, String closeFn,
             String checkSum, boolean isStaticLoad, long expirationTime, 
Expression... args) {
         super(name, args);
         this.dbName = dbName;
@@ -70,6 +76,8 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
         this.binaryType = binaryType;
         this.signature = signature;
         this.nullableMode = nullableMode;
+        this.volatility = volatility;
+        this.volatileIdentity = volatileIdentity;
         this.objectFile = objectFile;
         this.symbol = symbol;
         this.prepareFn = prepareFn;
@@ -86,10 +94,50 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
     public JavaUdtf withChildren(List<Expression> children) {
         Preconditions.checkArgument(children.size() == this.children.size());
         return new JavaUdtf(getName(), functionId, dbName, binaryType, 
signature, nullableMode,
+                volatility, volatileIdentity, objectFile, symbol, prepareFn, 
closeFn, checkSum, isStaticLoad,
+                expirationTime,
+                children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public VolatileIdentity getVolatileIdentity() {
+        return volatileIdentity;
+    }
+
+    @Override
+    public JavaUdtf withIgnoreUniqueId(boolean ignoreUniqueId) {
+        Preconditions.checkState(isVolatile(), "Only volatile Java UDTF can 
ignore unique id");
+        return new JavaUdtf(getName(), functionId, dbName, binaryType, 
signature, nullableMode,
+                volatility, 
volatileIdentity.withIgnoreUniqueId(ignoreUniqueId),
                 objectFile, symbol, prepareFn, closeFn, checkSum, 
isStaticLoad, expirationTime,
                 children.toArray(new Expression[0]));
     }
 
+    @Override
+    public JavaUdtf withFreshVolatileIdentity() {
+        if (volatility != FunctionVolatility.VOLATILE) {
+            return this;
+        }
+        return new JavaUdtf(getName(), functionId, dbName, binaryType, 
signature, nullableMode,
+                volatility, VolatileIdentity.newVolatileIdentity(),
+                objectFile, symbol, prepareFn, closeFn, checkSum, 
isStaticLoad, expirationTime,
+                children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof JavaUdtf)) {
+            return false;
+        }
+        JavaUdtf other = (JavaUdtf) o;
+        return volatileIdentity.equalsByIdentity(other.volatileIdentity, 
super.equals(o));
+    }
+
+    @Override
+    public int computeHashCode() {
+        return volatileIdentity.hashCodeByIdentity(super.computeHashCode());
+    }
+
     @Override
     public List<FunctionSignature> getSignatures() {
         return ImmutableList.of(signature);
@@ -125,6 +173,7 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
             expr.setStaticLoad(isStaticLoad);
             expr.setExpirationTime(expirationTime);
             expr.setUDTFunction(true);
+            expr.setVolatility(volatility);
             return expr;
         } catch (Exception e) {
             throw new AnalysisException(e.getMessage(), e.getCause());
@@ -152,6 +201,8 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
 
         JavaUdtf udf = new JavaUdtf(fnName, scalar.getId(), dbName, 
scalar.getBinaryType(), sig,
                 scalar.getNullableMode(),
+                scalar.getVolatility(),
+                Udf.createVolatileIdentity(scalar.getVolatility()),
                 scalar.getLocation() == null ? null : 
scalar.getLocation().getLocation(),
                 scalar.getSymbolName(),
                 scalar.getPrepareFnSymbol(),
@@ -170,6 +221,11 @@ public class JavaUdtf extends TableGeneratingFunction 
implements ExplicitlyCasta
         return nullableMode;
     }
 
+    @Override
+    public FunctionVolatility getVolatility() {
+        return volatility;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitJavaUdtf(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtfBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtfBuilder.java
index 5d85c0faf86..f00fa22cf5b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtfBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/JavaUdtfBuilder.java
@@ -88,7 +88,7 @@ public class JavaUdtfBuilder extends UdfBuilder {
         for (int i = 0; i < exprs.size(); ++i) {
             
processedExprs.add(TypeCoercionUtils.castIfNotSameType(exprs.get(i), 
argTypes.get(i)));
         }
-        return Pair.ofSame(udf.withChildren(processedExprs));
+        return 
Pair.ofSame(udf.withFreshVolatileIdentity().withChildren(processedExprs));
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdaf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdaf.java
index 456e0f1a6ea..39a27b72834 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdaf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdaf.java
@@ -22,11 +22,13 @@ import org.apache.doris.catalog.Function;
 import org.apache.doris.catalog.Function.NullableMode;
 import org.apache.doris.catalog.FunctionName;
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.catalog.FunctionVolatility;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.util.URI;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.Udf;
 import 
org.apache.doris.nereids.trees.expressions.functions.agg.AggregateFunction;
@@ -50,6 +52,8 @@ public class PythonUdaf extends AggregateFunction implements 
ExplicitlyCastableS
     private final FunctionSignature signature;
     private final DataType intermediateType;
     private final NullableMode nullableMode;
+    private final FunctionVolatility volatility;
+    private final VolatileIdentity volatileIdentity;
     private final String objectFile;
     private final String symbol;
     private final String initFn;
@@ -71,6 +75,7 @@ public class PythonUdaf extends AggregateFunction implements 
ExplicitlyCastableS
     public PythonUdaf(String name, long functionId, String dbName, 
Function.BinaryType binaryType,
                       FunctionSignature signature,
                       DataType intermediateType, NullableMode nullableMode,
+                      FunctionVolatility volatility, VolatileIdentity 
volatileIdentity,
                       String objectFile, String symbol,
                       String initFn, String updateFn, String mergeFn,
                       String serializeFn, String finalizeFn, String 
getValueFn, String removeFn,
@@ -83,6 +88,8 @@ public class PythonUdaf extends AggregateFunction implements 
ExplicitlyCastableS
         this.signature = signature;
         this.intermediateType = intermediateType == null ? 
signature.returnType : intermediateType;
         this.nullableMode = nullableMode;
+        this.volatility = volatility;
+        this.volatileIdentity = volatileIdentity;
         this.objectFile = objectFile;
         this.symbol = symbol;
         this.initFn = initFn;
@@ -126,11 +133,53 @@ public class PythonUdaf extends AggregateFunction 
implements ExplicitlyCastableS
     public PythonUdaf withDistinctAndChildren(boolean isDistinct, 
List<Expression> children) {
         Preconditions.checkArgument(children.size() == this.children.size());
         return new PythonUdaf(getName(), functionId, dbName, binaryType, 
signature, intermediateType, nullableMode,
-                objectFile, symbol, initFn, updateFn, mergeFn, serializeFn, 
finalizeFn, getValueFn, removeFn,
+                volatility, volatileIdentity, objectFile, symbol, initFn, 
updateFn, mergeFn, serializeFn, finalizeFn,
+                getValueFn, removeFn,
                 isDistinct, checkSum, isStaticLoad, expirationTime, 
runtimeVersion, functionCode,
                 children.toArray(new Expression[0]));
     }
 
+    @Override
+    public VolatileIdentity getVolatileIdentity() {
+        return volatileIdentity;
+    }
+
+    @Override
+    public PythonUdaf withIgnoreUniqueId(boolean ignoreUniqueId) {
+        Preconditions.checkState(isVolatile(), "Only volatile Python UDAF can 
ignore unique id");
+        return new PythonUdaf(getName(), functionId, dbName, binaryType, 
signature, intermediateType, nullableMode,
+                volatility, 
volatileIdentity.withIgnoreUniqueId(ignoreUniqueId),
+                objectFile, symbol, initFn, updateFn, mergeFn, serializeFn, 
finalizeFn, getValueFn, removeFn,
+                distinct, checkSum, isStaticLoad, expirationTime, 
runtimeVersion, functionCode,
+                children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public PythonUdaf withFreshVolatileIdentity() {
+        if (volatility != FunctionVolatility.VOLATILE) {
+            return this;
+        }
+        return new PythonUdaf(getName(), functionId, dbName, binaryType, 
signature, intermediateType, nullableMode,
+                volatility, VolatileIdentity.newVolatileIdentity(),
+                objectFile, symbol, initFn, updateFn, mergeFn, serializeFn, 
finalizeFn, getValueFn, removeFn,
+                distinct, checkSum, isStaticLoad, expirationTime, 
runtimeVersion, functionCode,
+                children.toArray(new Expression[0]));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof PythonUdaf)) {
+            return false;
+        }
+        PythonUdaf other = (PythonUdaf) o;
+        return volatileIdentity.equalsByIdentity(other.volatileIdentity, 
super.equals(o));
+    }
+
+    @Override
+    public int computeHashCode() {
+        return volatileIdentity.hashCodeByIdentity(super.computeHashCode());
+    }
+
     /**
      * translate catalog python udaf to nereids python udaf
      */
@@ -158,6 +207,8 @@ public class PythonUdaf extends AggregateFunction 
implements ExplicitlyCastableS
         PythonUdaf udaf = new PythonUdaf(fnName, aggregate.getId(), dbName, 
aggregate.getBinaryType(), sig,
                 intermediateType,
                 aggregate.getNullableMode(),
+                aggregate.getVolatility(),
+                Udf.createVolatileIdentity(aggregate.getVolatility()),
                 aggregate.getLocation() == null ? null : 
aggregate.getLocation().getLocation(),
                 aggregate.getSymbolName(),
                 aggregate.getInitFnSymbol(),
@@ -211,9 +262,15 @@ public class PythonUdaf extends AggregateFunction 
implements ExplicitlyCastableS
             expr.setExpirationTime(expirationTime);
             expr.setRuntimeVersion(runtimeVersion);
             expr.setFunctionCode(functionCode);
+            expr.setVolatility(volatility);
             return expr;
         } catch (Exception e) {
             throw new AnalysisException(e.getMessage(), e.getCause());
         }
     }
+
+    @Override
+    public FunctionVolatility getVolatility() {
+        return volatility;
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdafBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdafBuilder.java
index 73a50b6bc40..d18800fd429 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdafBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdafBuilder.java
@@ -79,7 +79,7 @@ public class PythonUdafBuilder extends UdfBuilder {
 
     @Override
     public Pair<PythonUdaf, PythonUdaf> build(String name, List<?> arguments) {
-        return Pair.ofSame((PythonUdaf) udaf.withChildren(
+        return Pair.ofSame((PythonUdaf) 
udaf.withFreshVolatileIdentity().withChildren(
                 arguments.stream()
                         .map(Expression.class::cast)
                         .collect(Collectors.toList()))
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdf.java
index 65c8f2f14d7..8bf05a8a6af 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdf.java
@@ -28,7 +28,6 @@ import org.apache.doris.common.util.URI;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
-import org.apache.doris.nereids.trees.expressions.VolatileExpression;
 import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.Udf;
@@ -46,7 +45,7 @@ import java.util.stream.Collectors;
 /**
  * Python UDF for Nereids
  */
-public class PythonUdf extends ScalarFunction implements 
ExplicitlyCastableSignature, Udf, VolatileExpression {
+public class PythonUdf extends ScalarFunction implements 
ExplicitlyCastableSignature, Udf {
     private final String dbName;
     private final long functionId;
     private final Function.BinaryType binaryType;
@@ -139,6 +138,7 @@ public class PythonUdf extends ScalarFunction implements 
ExplicitlyCastableSigna
     }
 
     /** Return a copy with a new per-call identity when this UDF is VOLATILE. 
*/
+    @Override
     public PythonUdf withFreshVolatileIdentity() {
         if (volatility != FunctionVolatility.VOLATILE) {
             return this;
@@ -149,11 +149,6 @@ public class PythonUdf extends ScalarFunction implements 
ExplicitlyCastableSigna
             runtimeVersion, functionCode, children.toArray(new Expression[0]));
     }
 
-    @Override
-    public boolean isDeterministic() {
-        return volatility == FunctionVolatility.IMMUTABLE;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (!(o instanceof PythonUdf)) {
@@ -188,7 +183,7 @@ public class PythonUdf extends ScalarFunction implements 
ExplicitlyCastableSigna
                 .toArray(SlotReference[]::new);
 
         PythonUdf udf = new PythonUdf(fnName, scalar.getId(), dbName, 
scalar.getBinaryType(), sig,
-                scalar.getNullableMode(), scalar.getVolatility(), 
createVolatileIdentity(scalar.getVolatility()),
+                scalar.getNullableMode(), scalar.getVolatility(), 
Udf.createVolatileIdentity(scalar.getVolatility()),
                 scalar.getLocation() == null ? null : 
scalar.getLocation().getLocation(),
                 scalar.getSymbolName(),
                 scalar.getPrepareFnSymbol(),
@@ -235,8 +230,8 @@ public class PythonUdf extends ScalarFunction implements 
ExplicitlyCastableSigna
         }
     }
 
-    private static VolatileIdentity createVolatileIdentity(FunctionVolatility 
volatility) {
-        return volatility == FunctionVolatility.VOLATILE
-                ? VolatileIdentity.newVolatileIdentity() : 
VolatileIdentity.NON_VOLATILE;
+    @Override
+    public FunctionVolatility getVolatility() {
+        return volatility;
     }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtf.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtf.java
index 74e662aee72..61aa694ff8e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtf.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtf.java
@@ -22,11 +22,13 @@ import org.apache.doris.catalog.Function;
 import org.apache.doris.catalog.Function.NullableMode;
 import org.apache.doris.catalog.FunctionName;
 import org.apache.doris.catalog.FunctionSignature;
+import org.apache.doris.catalog.FunctionVolatility;
 import org.apache.doris.catalog.Type;
 import org.apache.doris.common.util.URI;
 import org.apache.doris.nereids.exceptions.AnalysisException;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
 import 
org.apache.doris.nereids.trees.expressions.functions.ExplicitlyCastableSignature;
 import org.apache.doris.nereids.trees.expressions.functions.Udf;
 import 
org.apache.doris.nereids.trees.expressions.functions.generator.TableGeneratingFunction;
@@ -49,6 +51,8 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
     private final Function.BinaryType binaryType;
     private final FunctionSignature signature;
     private final NullableMode nullableMode;
+    private final FunctionVolatility volatility;
+    private final VolatileIdentity volatileIdentity;
     private final String objectFile;
     private final String symbol;
     private final String prepareFn;
@@ -64,7 +68,9 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
      */
     public PythonUdtf(String name, long functionId, String dbName, 
Function.BinaryType binaryType,
             FunctionSignature signature,
-            NullableMode nullableMode, String objectFile, String symbol, 
String prepareFn, String closeFn,
+            NullableMode nullableMode, FunctionVolatility volatility, 
VolatileIdentity volatileIdentity,
+            String objectFile, String symbol,
+            String prepareFn, String closeFn,
             String checkSum, boolean isStaticLoad, long expirationTime,
             String runtimeVersion, String functionCode, Expression... args) {
         super(name, args);
@@ -73,6 +79,8 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
         this.binaryType = binaryType;
         this.signature = signature;
         this.nullableMode = nullableMode;
+        this.volatility = volatility;
+        this.volatileIdentity = volatileIdentity;
         this.objectFile = objectFile;
         this.symbol = symbol;
         this.prepareFn = prepareFn;
@@ -91,10 +99,50 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
     public PythonUdtf withChildren(List<Expression> children) {
         Preconditions.checkArgument(children.size() == this.children.size());
         return new PythonUdtf(getName(), functionId, dbName, binaryType, 
signature, nullableMode,
+                volatility, volatileIdentity, objectFile, symbol, prepareFn, 
closeFn, checkSum, isStaticLoad,
+                expirationTime,
+                runtimeVersion, functionCode, children.toArray(new 
Expression[0]));
+    }
+
+    @Override
+    public VolatileIdentity getVolatileIdentity() {
+        return volatileIdentity;
+    }
+
+    @Override
+    public PythonUdtf withIgnoreUniqueId(boolean ignoreUniqueId) {
+        Preconditions.checkState(isVolatile(), "Only volatile Python UDTF can 
ignore unique id");
+        return new PythonUdtf(getName(), functionId, dbName, binaryType, 
signature, nullableMode,
+                volatility, 
volatileIdentity.withIgnoreUniqueId(ignoreUniqueId),
                 objectFile, symbol, prepareFn, closeFn, checkSum, 
isStaticLoad, expirationTime,
                 runtimeVersion, functionCode, children.toArray(new 
Expression[0]));
     }
 
+    @Override
+    public PythonUdtf withFreshVolatileIdentity() {
+        if (volatility != FunctionVolatility.VOLATILE) {
+            return this;
+        }
+        return new PythonUdtf(getName(), functionId, dbName, binaryType, 
signature, nullableMode,
+                volatility, VolatileIdentity.newVolatileIdentity(),
+                objectFile, symbol, prepareFn, closeFn, checkSum, 
isStaticLoad, expirationTime,
+                runtimeVersion, functionCode, children.toArray(new 
Expression[0]));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof PythonUdtf)) {
+            return false;
+        }
+        PythonUdtf other = (PythonUdtf) o;
+        return volatileIdentity.equalsByIdentity(other.volatileIdentity, 
super.equals(o));
+    }
+
+    @Override
+    public int computeHashCode() {
+        return volatileIdentity.hashCodeByIdentity(super.computeHashCode());
+    }
+
     @Override
     public List<FunctionSignature> getSignatures() {
         return ImmutableList.of(signature);
@@ -132,6 +180,7 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
             expr.setUDTFunction(true);
             expr.setRuntimeVersion(runtimeVersion);
             expr.setFunctionCode(functionCode);
+            expr.setVolatility(volatility);
             return expr;
         } catch (Exception e) {
             throw new AnalysisException(e.getMessage(), e.getCause());
@@ -159,6 +208,8 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
 
         PythonUdtf udtf = new PythonUdtf(fnName, scalar.getId(), dbName, 
scalar.getBinaryType(), sig,
                 scalar.getNullableMode(),
+                scalar.getVolatility(),
+                Udf.createVolatileIdentity(scalar.getVolatility()),
                 scalar.getLocation() == null ? null : 
scalar.getLocation().getLocation(),
                 scalar.getSymbolName(),
                 scalar.getPrepareFnSymbol(),
@@ -179,6 +230,11 @@ public class PythonUdtf extends TableGeneratingFunction 
implements ExplicitlyCas
         return nullableMode;
     }
 
+    @Override
+    public FunctionVolatility getVolatility() {
+        return volatility;
+    }
+
     @Override
     public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
         return visitor.visitPythonUdtf(this, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtfBuilder.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtfBuilder.java
index 3c032ba18ab..dd9638f3c20 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtfBuilder.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/udf/PythonUdtfBuilder.java
@@ -88,7 +88,7 @@ public class PythonUdtfBuilder extends UdfBuilder {
         for (int i = 0; i < exprs.size(); ++i) {
             
processedExprs.add(TypeCoercionUtils.castIfNotSameType(exprs.get(i), 
argTypes.get(i)));
         }
-        return Pair.ofSame(udtf.withChildren(processedExprs));
+        return 
Pair.ofSame(udtf.withFreshVolatileIdentity().withChildren(processedExprs));
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateFunctionCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateFunctionCommand.java
index 027056ba321..0cbfb30d59f 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateFunctionCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/CreateFunctionCommand.java
@@ -185,6 +185,7 @@ public class CreateFunctionCommand extends Command 
implements ForwardWithSync {
     // if not, will core dump when input is not null column, but need return 
null
     // like https://github.com/apache/doris/pull/14002/files
     private NullableMode returnNullMode = NullableMode.ALWAYS_NULLABLE;
+    // Keep IMMUTABLE as the UDAF/UDTF default for compatibility with previous 
behavior.
     private FunctionVolatility volatility = FunctionVolatility.IMMUTABLE;
     private String runtimeVersion;
     private String functionCode;
@@ -324,10 +325,6 @@ public class CreateFunctionCommand extends Command 
implements ForwardWithSync {
             throw new AnalysisException("do not support 'NATIVE' udf type 
after doris version 1.2.0,"
                     + "please use JAVA_UDF or RPC instead");
         }
-        if (properties.containsKey(VOLATILITY) && (isAggregate || 
isTableFunction)) {
-            throw new AnalysisException("volatility property only supports 
scalar JAVA_UDF and PYTHON_UDF");
-        }
-
         userFile = properties.getOrDefault(FILE_KEY, 
properties.get(OBJECT_FILE_KEY));
         originalUserFile = userFile; // Keep original jar name for BE
         // Inline Python code is authoritative. Keep FILE in metadata for 
replay, but do not load or validate it here.
@@ -347,9 +344,7 @@ public class CreateFunctionCommand extends Command 
implements ForwardWithSync {
         if (binaryType == Function.BinaryType.JAVA_UDF) {
             FunctionUtil.checkEnableJavaUdf();
             checkUdfSupportedTypes();
-            if (!isAggregate && !isTableFunction) {
-                volatility = analyzeVolatility();
-            }
+            volatility = analyzeVolatility(defaultVolatility());
 
             // always_nullable the default value is true, equal null means true
             Boolean isReturnNull = parseBooleanFromProperties(IS_RETURN_NULL);
@@ -365,9 +360,7 @@ public class CreateFunctionCommand extends Command 
implements ForwardWithSync {
         } else if (binaryType == Function.BinaryType.PYTHON_UDF) {
             FunctionUtil.checkEnablePythonUdf();
             checkUdfSupportedTypes();
-            if (!isAggregate && !isTableFunction) {
-                volatility = analyzeVolatility();
-            }
+            volatility = analyzeVolatility(defaultVolatility());
 
             // always_nullable the default value is true, equal null means true
             Boolean isReturnNull = parseBooleanFromProperties(IS_RETURN_NULL);
@@ -389,9 +382,13 @@ public class CreateFunctionCommand extends Command 
implements ForwardWithSync {
         }
     }
 
-    private FunctionVolatility analyzeVolatility() throws AnalysisException {
+    private FunctionVolatility defaultVolatility() {
+        return isAggregate || isTableFunction ? FunctionVolatility.IMMUTABLE : 
FunctionVolatility.VOLATILE;
+    }
+
+    private FunctionVolatility analyzeVolatility(FunctionVolatility 
defaultVolatility) throws AnalysisException {
         if (!properties.containsKey(VOLATILITY)) {
-            return FunctionVolatility.VOLATILE;
+            return defaultVolatility;
         }
         try {
             return FunctionVolatility.fromString(properties.get(VOLATILITY));
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommand.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommand.java
index 081d482308a..58a849ce308 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommand.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommand.java
@@ -295,9 +295,8 @@ public class ShowFunctionsCommand extends ShowCommand {
         }
         if (function instanceof ScalarFunction) {
             ScalarFunction scalarFunction = (ScalarFunction) function;
-            if (!scalarFunction.isUDTFunction()
-                    && (function.getBinaryType() == 
Function.BinaryType.JAVA_UDF
-                    || function.getBinaryType() == 
Function.BinaryType.PYTHON_UDF)) {
+            if (function.getBinaryType() == Function.BinaryType.JAVA_UDF
+                    || function.getBinaryType() == 
Function.BinaryType.PYTHON_UDF) {
                 properties.put("VOLATILITY", function.getVolatility().toSql());
             }
             properties.put("SYMBOL", 
Strings.nullToEmpty(scalarFunction.getSymbolName()));
@@ -311,6 +310,10 @@ public class ShowFunctionsCommand extends ShowCommand {
 
         if (function instanceof AggregateFunction) {
             AggregateFunction aggregateFunction = (AggregateFunction) function;
+            if (function.getBinaryType() == Function.BinaryType.JAVA_UDF
+                    || function.getBinaryType() == 
Function.BinaryType.PYTHON_UDF) {
+                properties.put("VOLATILITY", function.getVolatility().toSql());
+            }
             properties.put("INIT_FN", 
Strings.nullToEmpty(aggregateFunction.getInitFnSymbol()));
             properties.put("UPDATE_FN", 
Strings.nullToEmpty(aggregateFunction.getUpdateFnSymbol()));
             properties.put("MERGE_FN", 
Strings.nullToEmpty(aggregateFunction.getMergeFnSymbol()));
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateFunctionTest.java 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateFunctionTest.java
index e6741b9e54c..72d29a16f2b 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateFunctionTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateFunctionTest.java
@@ -128,6 +128,17 @@ public class CreateFunctionTest {
                 + "properties('type'='PYTHON_UDF', 'symbol'='evaluate', 
'runtime_version'='3.10.2');";
         createFunction(defaultVolatileSql, ctx);
         Assert.assertEquals(FunctionVolatility.VOLATILE, findFunction(db, 
"py_default").getVolatility());
+
+        String defaultImmutableUdafSql = "create aggregate function 
db1.py_agg_default(int) returns int "
+                + "properties('type'='PYTHON_UDF', 'symbol'='Agg', 
'runtime_version'='3.10.2');";
+        createFunction(defaultImmutableUdafSql, ctx);
+        Assert.assertEquals(FunctionVolatility.IMMUTABLE, findFunction(db, 
"py_agg_default").getVolatility());
+
+        String stableUdtfSql = "create tables function 
db1.py_table_stable(int) returns array<int> "
+                + "properties('type'='PYTHON_UDF', 'symbol'='evaluate', 
'runtime_version'='3.10.2', "
+                + "'volatility'='stable');";
+        createFunction(stableUdtfSql, ctx);
+        Assert.assertEquals(FunctionVolatility.STABLE, findFunction(db, 
"py_table_stable").getVolatility());
     }
 
     @Test
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/FunctionToSqlConverterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/FunctionToSqlConverterTest.java
index d60a4502d2d..9013c7671db 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/FunctionToSqlConverterTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/FunctionToSqlConverterTest.java
@@ -154,7 +154,7 @@ public class FunctionToSqlConverterTest {
         ScalarFunction fn = ScalarFunction.createUdf(BinaryType.JAVA_UDF, 
name, argTypes,
                 Type.INT, false, null, "com.example.TableFn", null, null);
         fn.setUDTFunction(true);
-        fn.setVolatility(FunctionVolatility.IMMUTABLE);
+        fn.setVolatility(FunctionVolatility.STABLE);
 
         String sql = FunctionToSqlConverter.toSql(fn, true);
 
@@ -162,7 +162,7 @@ public class FunctionToSqlConverterTest {
         Assertions.assertTrue(sql.contains("java_table_fn(int)"));
         Assertions.assertTrue(sql.contains("RETURNS array<int>"));
         Assertions.assertTrue(sql.contains("\"TYPE\"=\"JAVA_UDF\""));
-        Assertions.assertFalse(sql.contains("VOLATILITY"));
+        Assertions.assertTrue(sql.contains("\"VOLATILITY\"=\"stable\""));
     }
 
     @Test
@@ -174,7 +174,7 @@ public class FunctionToSqlConverterTest {
         fn.setUDTFunction(true);
         fn.setRuntimeVersion("3.10.2");
         fn.setFunctionCode("def evaluate(x):\n    yield x");
-        fn.setVolatility(FunctionVolatility.IMMUTABLE);
+        fn.setVolatility(FunctionVolatility.VOLATILE);
 
         String sql = FunctionToSqlConverter.toSql(fn, false);
 
@@ -182,10 +182,10 @@ public class FunctionToSqlConverterTest {
         Assertions.assertTrue(sql.contains("py_table_fn(int)"));
         Assertions.assertTrue(sql.contains("RETURNS array<int>"));
         Assertions.assertTrue(sql.contains("\"RUNTIME_VERSION\"=\"3.10.2\""));
+        Assertions.assertTrue(sql.contains("\"VOLATILITY\"=\"volatile\""));
         Assertions.assertTrue(sql.contains("\"TYPE\"=\"PYTHON_UDF\""));
         Assertions.assertTrue(sql.contains("AS $$\ndef evaluate(x):\n    yield 
x\n$$;"));
         Assertions.assertFalse(sql.contains("\"FILE\"="));
-        Assertions.assertFalse(sql.contains("VOLATILITY"));
     }
 
     // ======================== ScalarFunction — IF NOT EXISTS 
========================
@@ -263,6 +263,7 @@ public class FunctionToSqlConverterTest {
                 .symbolName("com.example.MySum")
                 .location(URI.create("file:///tmp/java-udaf.jar"))
                 .build();
+        fn.setVolatility(FunctionVolatility.STABLE);
 
         String sql = FunctionToSqlConverter.toSql(fn, false);
 
@@ -272,6 +273,7 @@ public class FunctionToSqlConverterTest {
         
Assertions.assertTrue(sql.contains("\"SYMBOL\"=\"com.example.MySum\""));
         
Assertions.assertTrue(sql.contains("\"FILE\"=\"file:///tmp/java-udaf.jar\""));
         Assertions.assertTrue(sql.contains("\"TYPE\"=\"JAVA_UDF\""));
+        Assertions.assertTrue(sql.contains("\"VOLATILITY\"=\"stable\""));
         Assertions.assertTrue(sql.contains("\"ALWAYS_NULLABLE\"="));
         Assertions.assertFalse(sql.contains("INIT_FN"));
         Assertions.assertFalse(sql.contains("UPDATE_FN"));
@@ -313,11 +315,13 @@ public class FunctionToSqlConverterTest {
         fn.setRuntimeVersion("3.10.2");
         fn.setExpirationTime(45);
         fn.setFunctionCode("class SumState:\n    pass");
+        fn.setVolatility(FunctionVolatility.IMMUTABLE);
 
         String sql = FunctionToSqlConverter.toSql(fn, false);
 
         Assertions.assertTrue(sql.contains("\"RUNTIME_VERSION\"=\"3.10.2\""));
         Assertions.assertTrue(sql.contains("\"EXPIRATION_TIME\"=\"45\""));
+        Assertions.assertTrue(sql.contains("\"VOLATILITY\"=\"immutable\""));
         Assertions.assertTrue(sql.contains("\"TYPE\"=\"PYTHON_UDF\""));
         Assertions.assertTrue(sql.contains("AS $$\nclass SumState:\n    
pass\n$$;"));
         Assertions.assertFalse(sql.contains("\"FILE\"="));
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/udf/UdfVolatilityTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/udf/UdfVolatilityTest.java
index 290650435e9..3c54a1acd18 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/udf/UdfVolatilityTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/udf/UdfVolatilityTest.java
@@ -23,6 +23,7 @@ import org.apache.doris.catalog.FunctionSignature;
 import org.apache.doris.catalog.FunctionVolatility;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.VolatileIdentity;
+import org.apache.doris.nereids.trees.expressions.functions.Udf;
 import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.util.ExpressionUtils;
@@ -37,6 +38,7 @@ class UdfVolatilityTest {
         PythonUdf udf = pythonUdf(FunctionVolatility.IMMUTABLE, 
VolatileIdentity.NON_VOLATILE);
 
         Assertions.assertTrue(udf.isDeterministic());
+        Assertions.assertEquals(FunctionVolatility.IMMUTABLE, 
udf.getVolatility());
         Assertions.assertFalse(udf.containsVolatileExpression());
         Assertions.assertEquals(PythonUdf.class, new 
PythonUdfBuilder(udf).functionClass());
     }
@@ -47,6 +49,7 @@ class UdfVolatilityTest {
         PythonUdf second = pythonUdf(FunctionVolatility.VOLATILE, 
VolatileIdentity.newVolatileIdentity());
 
         Assertions.assertFalse(first.isDeterministic());
+        Assertions.assertEquals(FunctionVolatility.VOLATILE, 
first.getVolatility());
         Assertions.assertTrue(first.containsVolatileExpression());
         Assertions.assertNotEquals(first, second);
 
@@ -69,9 +72,45 @@ class UdfVolatilityTest {
         JavaUdf udf = javaUdf(FunctionVolatility.STABLE, 
VolatileIdentity.NON_VOLATILE);
 
         Assertions.assertFalse(udf.isDeterministic());
+        Assertions.assertEquals(FunctionVolatility.STABLE, 
udf.getVolatility());
         Assertions.assertFalse(udf.containsVolatileExpression());
     }
 
+    @Test
+    void testPythonUdafVolatility() {
+        PythonUdaf immutable = pythonUdaf(FunctionVolatility.IMMUTABLE);
+        PythonUdaf stable = pythonUdaf(FunctionVolatility.STABLE);
+
+        Assertions.assertTrue(immutable.isDeterministic());
+        Assertions.assertFalse(stable.isDeterministic());
+        Assertions.assertEquals(FunctionVolatility.STABLE, 
stable.getCatalogFunction().getVolatility());
+        Assertions.assertFalse(stable.containsVolatileExpression());
+    }
+
+    @Test
+    void testPythonUdtfVolatility() {
+        PythonUdtf immutable = pythonUdtf(FunctionVolatility.IMMUTABLE);
+        PythonUdtf volatileUdtf = pythonUdtf(FunctionVolatility.VOLATILE);
+
+        Assertions.assertTrue(immutable.isDeterministic());
+        Assertions.assertFalse(volatileUdtf.isDeterministic());
+        Assertions.assertEquals(FunctionVolatility.VOLATILE, 
volatileUdtf.getCatalogFunction().getVolatility());
+        Assertions.assertTrue(volatileUdtf.containsVolatileExpression());
+    }
+
+    @Test
+    void testVolatilePythonUdafUsesUniqueIdentity() {
+        PythonUdaf first = pythonUdaf(FunctionVolatility.VOLATILE);
+        PythonUdaf second = pythonUdaf(FunctionVolatility.VOLATILE);
+
+        Assertions.assertTrue(first.containsVolatileExpression());
+        Assertions.assertNotEquals(first, second);
+
+        Expression ignoredFirst = 
ExpressionUtils.setIgnoreUniqueIdForVolatileExpression(first, true);
+        Expression ignoredSecond = 
ExpressionUtils.setIgnoreUniqueIdForVolatileExpression(second, true);
+        Assertions.assertEquals(ignoredFirst, ignoredSecond);
+    }
+
     private PythonUdf pythonUdf(FunctionVolatility volatility, 
VolatileIdentity volatileIdentity) {
         return new PythonUdf("py_fn", 1, "db1", 
Function.BinaryType.PYTHON_UDF, signature(),
                 NullableMode.ALWAYS_NULLABLE, volatility, volatileIdentity,
@@ -85,6 +124,19 @@ class UdfVolatilityTest {
                 null, "evaluate", null, null, "", false, 360, new 
IntegerLiteral(1));
     }
 
+    private PythonUdaf pythonUdaf(FunctionVolatility volatility) {
+        return new PythonUdaf("py_agg", 1, "db1", 
Function.BinaryType.PYTHON_UDF, signature(),
+                IntegerType.INSTANCE, NullableMode.ALWAYS_NULLABLE, 
volatility, Udf.createVolatileIdentity(volatility),
+                null, "Agg", null, null, null, null, null, null, null, false, 
"", false, 360,
+                "3.10.2", "", new IntegerLiteral(1));
+    }
+
+    private PythonUdtf pythonUdtf(FunctionVolatility volatility) {
+        return new PythonUdtf("py_table", 1, "db1", 
Function.BinaryType.PYTHON_UDF, signature(),
+                NullableMode.ALWAYS_NULLABLE, volatility, 
Udf.createVolatileIdentity(volatility),
+                null, "evaluate", null, null, "", false, 360, "3.10.2", "", 
new IntegerLiteral(1));
+    }
+
     private FunctionSignature signature() {
         return 
FunctionSignature.ret(IntegerType.INSTANCE).args(IntegerType.INSTANCE);
     }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommandTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommandTest.java
index 716b9fa24da..3c1f3ba6b88 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommandTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/commands/ShowFunctionsCommandTest.java
@@ -22,6 +22,7 @@ import org.apache.doris.analysis.UserDesc;
 import org.apache.doris.analysis.UserIdentity;
 import org.apache.doris.catalog.AccessPrivilege;
 import org.apache.doris.catalog.AccessPrivilegeWithCols;
+import org.apache.doris.catalog.AggregateFunction;
 import org.apache.doris.catalog.Env;
 import org.apache.doris.catalog.Function;
 import org.apache.doris.catalog.FunctionName;
@@ -130,35 +131,55 @@ public class ShowFunctionsCommandTest extends 
TestWithFeService {
     }
 
     @Test
-    void testBuildProperties_javaUdtfDoesNotEmitVolatility() {
+    void testBuildProperties_javaUdtfEmitsVolatility() {
         ShowFunctionsCommand sf = new ShowFunctionsCommand("test", true, null);
         ScalarFunction fn = 
ScalarFunction.createUdf(Function.BinaryType.JAVA_UDF,
                 new FunctionName("test", "java_table_fn"), new Type[] 
{Type.INT},
                 Type.INT, false, null, "com.example.TableFn", null, null);
         fn.setUDTFunction(true);
-        fn.setVolatility(FunctionVolatility.IMMUTABLE);
+        fn.setVolatility(FunctionVolatility.STABLE);
 
         String properties = sf.buildPropertiesForTest(fn);
 
         
Assertions.assertTrue(properties.contains("SYMBOL=com.example.TableFn"));
-        Assertions.assertFalse(properties.contains("VOLATILITY"));
+        Assertions.assertTrue(properties.contains("VOLATILITY=stable"));
     }
 
     @Test
-    void testBuildProperties_pythonUdtfDoesNotEmitVolatility() {
+    void testBuildProperties_pythonUdtfEmitsVolatility() {
         ShowFunctionsCommand sf = new ShowFunctionsCommand("test", true, null);
         ScalarFunction fn = 
ScalarFunction.createUdf(Function.BinaryType.PYTHON_UDF,
                 new FunctionName("test", "py_table_fn"), new Type[] {Type.INT},
                 Type.INT, false, null, "evaluate", null, null);
         fn.setUDTFunction(true);
         fn.setRuntimeVersion("3.10.2");
-        fn.setVolatility(FunctionVolatility.IMMUTABLE);
+        fn.setVolatility(FunctionVolatility.VOLATILE);
 
         String properties = sf.buildPropertiesForTest(fn);
 
         Assertions.assertTrue(properties.contains("RUNTIME_VERSION=3.10.2"));
         Assertions.assertTrue(properties.contains("SYMBOL=evaluate"));
-        Assertions.assertFalse(properties.contains("VOLATILITY"));
+        Assertions.assertTrue(properties.contains("VOLATILITY=volatile"));
+    }
+
+    @Test
+    void testBuildProperties_udafEmitsVolatility() {
+        ShowFunctionsCommand sf = new ShowFunctionsCommand("test", true, null);
+        AggregateFunction fn = 
AggregateFunction.AggregateFunctionBuilder.createUdfBuilder()
+                .binaryType(Function.BinaryType.PYTHON_UDF)
+                .name(new FunctionName("test", "py_agg_fn"))
+                .argsType(new Type[] {Type.INT})
+                .retType(Type.INT)
+                .intermediateType(Type.INT)
+                .hasVarArgs(false)
+                .symbolName("Agg")
+                .build();
+        fn.setVolatility(FunctionVolatility.STABLE);
+
+        String properties = sf.buildPropertiesForTest(fn);
+
+        Assertions.assertTrue(properties.contains("SYMBOL=Agg"));
+        Assertions.assertTrue(properties.contains("VOLATILITY=stable"));
     }
 
     @Test


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to