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

dataroaring pushed a commit to branch branch-3.0
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-3.0 by this push:
     new 459632e0f4e [feat](jdbc) support custom function rules in catalog 
properties (#51471) (#51918)
459632e0f4e is described below

commit 459632e0f4e324e267c76c5b2fadbacc795587f7
Author: Mingyu Chen (Rayner) <morning...@163.com>
AuthorDate: Fri Jun 20 09:26:45 2025 +0800

    [feat](jdbc) support custom function rules in catalog properties (#51471) 
(#51918)
    
    bp #51471
---
 .../org/apache/doris/catalog/JdbcResource.java     |   1 +
 .../java/org/apache/doris/catalog/JdbcTable.java   |  20 +
 .../apache/doris/datasource/ExternalCatalog.java   |  68 ++--
 .../apache/doris/datasource/ExternalDatabase.java  |  72 ++--
 .../doris/datasource/ExternalFunctionRules.java    | 287 +++++++++++++
 .../doris/datasource/jdbc/JdbcExternalCatalog.java |  11 +
 .../doris/datasource/jdbc/JdbcExternalTable.java   |   1 +
 .../jdbc/source/JdbcFunctionPushDownRule.java      |  84 ++--
 .../doris/datasource/jdbc/source/JdbcScanNode.java |  10 +-
 .../ExternalFunctionPushDownRulesTest.java         | 431 ++++++++++++++++++++
 .../ExternalFunctionRewriteRulesTest.java          | 443 +++++++++++++++++++++
 ...bleNameComparedLowercaseMetaCacheFalseTest.java |   1 -
 .../jdbc/test_clickhouse_jdbc_catalog.groovy       |  71 +++-
 .../jdbc/test_jdbc_query_mysql.groovy              | 134 ++++++-
 .../jdbc/test_mysql_jdbc_catalog.groovy            |  76 +++-
 .../jdbc/test_oracle_jdbc_catalog.groovy           |  72 +++-
 16 files changed, 1644 insertions(+), 138 deletions(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
index bbd3e6df802..c3c8b4d49a6 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcResource.java
@@ -109,6 +109,7 @@ public class JdbcResource extends Resource {
     public static final String CHECK_SUM = "checksum";
     public static final String CREATE_TIME = "create_time";
     public static final String TEST_CONNECTION = "test_connection";
+    public static final String FUNCTION_RULES = "function_rules";
 
     private static final ImmutableList<String> ALL_PROPERTIES = new 
ImmutableList.Builder<String>().add(
             JDBC_URL,
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcTable.java 
b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcTable.java
index 6dce40a2684..adf013576b3 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcTable.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/JdbcTable.java
@@ -22,6 +22,7 @@ import org.apache.doris.common.DdlException;
 import org.apache.doris.common.FeConstants;
 import org.apache.doris.common.io.DeepCopy;
 import org.apache.doris.common.io.Text;
+import org.apache.doris.datasource.ExternalFunctionRules;
 import org.apache.doris.thrift.TJdbcTable;
 import org.apache.doris.thrift.TOdbcTableType;
 import org.apache.doris.thrift.TTableDescriptor;
@@ -103,6 +104,8 @@ public class JdbcTable extends Table {
     private int connectionPoolMaxLifeTime;
     private boolean connectionPoolKeepAlive;
 
+    private ExternalFunctionRules functionRules;
+
     static {
         Map<String, TOdbcTableType> tempMap = new CaseInsensitiveMap();
         tempMap.put("mysql", TOdbcTableType.MYSQL);
@@ -128,6 +131,8 @@ public class JdbcTable extends Table {
             throws DdlException {
         super(id, name, TableType.JDBC, schema);
         validate(properties);
+        // check and set external function rules
+        checkAndSetExternalFunctionRules(properties);
     }
 
     public JdbcTable(long id, String name, List<Column> schema, TableType 
type) {
@@ -412,6 +417,12 @@ public class JdbcTable extends Table {
         }
     }
 
+    private void checkAndSetExternalFunctionRules(Map<String, String> 
properties) throws DdlException {
+        
ExternalFunctionRules.check(properties.getOrDefault(JdbcResource.FUNCTION_RULES,
 ""));
+        this.functionRules = ExternalFunctionRules.create(jdbcTypeName,
+                properties.getOrDefault(JdbcResource.FUNCTION_RULES, ""));
+    }
+
     /**
      * Formats the provided name (for example, a database, table, or schema 
name) according to the specified parameters.
      *
@@ -509,4 +520,13 @@ public class JdbcTable extends Table {
     public static String formatNameWithRemoteName(String remoteName, String 
wrapStart, String wrapEnd) {
         return wrapStart + remoteName + wrapEnd;
     }
+
+    // This is used when converting JdbcExternalTable to JdbcTable.
+    public void setExternalFunctionRules(ExternalFunctionRules functionRules) {
+        this.functionRules = functionRules;
+    }
+
+    public ExternalFunctionRules getExternalFunctionRules() {
+        return functionRules;
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
index a96221057ea..0bf975a75d4 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalCatalog.java
@@ -166,6 +166,8 @@ public abstract class ExternalCatalog
     private volatile Configuration cachedConf = null;
     private byte[] confLock = new byte[0];
 
+    private volatile boolean isInitializing = false;
+
     public ExternalCatalog() {
     }
 
@@ -289,38 +291,46 @@ public abstract class ExternalCatalog
      * So you have to make sure the client of third system is initialized 
before any method was called.
      */
     public final synchronized void makeSureInitialized() {
-        initLocalObjects();
-        if (!initialized) {
-            if (useMetaCache.get()) {
-                if (metaCache == null) {
-                    metaCache = 
Env.getCurrentEnv().getExtMetaCacheMgr().buildMetaCache(
-                            name,
-                            OptionalLong.of(86400L),
-                            
OptionalLong.of(Config.external_cache_expire_time_minutes_after_access * 60L),
-                            Config.max_meta_object_cache_num,
-                            ignored -> getFilteredDatabaseNames(),
-                            localDbName -> Optional.ofNullable(
-                                    buildDbForInit(null, localDbName, 
Util.genIdByName(name, localDbName), logType,
-                                            true)),
-                            (key, value, cause) -> value.ifPresent(v -> 
v.setUnInitialized(invalidCacheInInit)));
-                }
-                setLastUpdateTime(System.currentTimeMillis());
-            } else {
-                if (!Env.getCurrentEnv().isMaster()) {
-                    // Forward to master and wait the journal to replay.
-                    int waitTimeOut = ConnectContext.get() == null ? 300 : 
ConnectContext.get().getExecTimeout();
-                    MasterCatalogExecutor remoteExecutor = new 
MasterCatalogExecutor(waitTimeOut * 1000);
-                    try {
-                        remoteExecutor.forward(id, -1);
-                    } catch (Exception e) {
-                        Util.logAndThrowRuntimeException(LOG,
-                                String.format("failed to forward init catalog 
%s operation to master.", name), e);
+        if (isInitializing) {
+            return;
+        }
+        isInitializing = true;
+        try {
+            initLocalObjects();
+            if (!initialized) {
+                if (useMetaCache.get()) {
+                    if (metaCache == null) {
+                        metaCache = 
Env.getCurrentEnv().getExtMetaCacheMgr().buildMetaCache(
+                                name,
+                                OptionalLong.of(86400L),
+                                
OptionalLong.of(Config.external_cache_expire_time_minutes_after_access * 60L),
+                                Config.max_meta_object_cache_num,
+                                ignored -> getFilteredDatabaseNames(),
+                                localDbName -> Optional.ofNullable(
+                                        buildDbForInit(null, localDbName, 
Util.genIdByName(name, localDbName), logType,
+                                                true)),
+                                (key, value, cause) -> value.ifPresent(v -> 
v.setUnInitialized(invalidCacheInInit)));
+                    }
+                    setLastUpdateTime(System.currentTimeMillis());
+                } else {
+                    if (!Env.getCurrentEnv().isMaster()) {
+                        // Forward to master and wait the journal to replay.
+                        int waitTimeOut = ConnectContext.get() == null ? 300 : 
ConnectContext.get().getExecTimeout();
+                        MasterCatalogExecutor remoteExecutor = new 
MasterCatalogExecutor(waitTimeOut * 1000);
+                        try {
+                            remoteExecutor.forward(id, -1);
+                        } catch (Exception e) {
+                            Util.logAndThrowRuntimeException(LOG,
+                                    String.format("failed to forward init 
catalog %s operation to master.", name), e);
+                        }
+                        return;
                     }
-                    return;
+                    init();
                 }
-                init();
+                initialized = true;
             }
-            initialized = true;
+        } finally {
+            isInitializing = false;
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java
index 5d5348b1f0d..ece9d7e265c 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalDatabase.java
@@ -101,6 +101,8 @@ public abstract class ExternalDatabase<T extends 
ExternalTable>
 
     private MetaCache<T> metaCache;
 
+    private volatile boolean isInitializing = false;
+
     /**
      * Create external database.
      *
@@ -154,39 +156,49 @@ public abstract class ExternalDatabase<T extends 
ExternalTable>
     }
 
     public final synchronized void makeSureInitialized() {
-        extCatalog.makeSureInitialized();
-        if (!initialized) {
-            if (extCatalog.getUseMetaCache().get()) {
-                if (metaCache == null) {
-                    metaCache = 
Env.getCurrentEnv().getExtMetaCacheMgr().buildMetaCache(
-                            name,
-                            OptionalLong.of(86400L),
-                            
OptionalLong.of(Config.external_cache_expire_time_minutes_after_access * 60L),
-                            Config.max_meta_object_cache_num,
-                            ignored -> listTableNames(),
-                            localTableName -> Optional.ofNullable(
-                                    buildTableForInit(null, localTableName,
-                                            
Util.genIdByName(extCatalog.getName(), name, localTableName), extCatalog,
-                                            this, true)),
-                            (key, value, cause) -> 
value.ifPresent(ExternalTable::unsetObjectCreated));
-                }
-                setLastUpdateTime(System.currentTimeMillis());
-            } else {
-                if (!Env.getCurrentEnv().isMaster()) {
-                    // Forward to master and wait the journal to replay.
-                    int waitTimeOut = ConnectContext.get() == null ? 300 : 
ConnectContext.get().getExecTimeout();
-                    MasterCatalogExecutor remoteExecutor = new 
MasterCatalogExecutor(waitTimeOut * 1000);
-                    try {
-                        remoteExecutor.forward(extCatalog.getId(), id);
-                    } catch (Exception e) {
-                        Util.logAndThrowRuntimeException(LOG,
-                                String.format("failed to forward init external 
db %s operation to master", name), e);
+        if (isInitializing) {
+            return;
+        }
+        isInitializing = true;
+        try {
+            extCatalog.makeSureInitialized();
+            if (!initialized) {
+                if (extCatalog.getUseMetaCache().get()) {
+                    if (metaCache == null) {
+                        metaCache = 
Env.getCurrentEnv().getExtMetaCacheMgr().buildMetaCache(
+                                name,
+                                OptionalLong.of(86400L),
+                                
OptionalLong.of(Config.external_cache_expire_time_minutes_after_access * 60L),
+                                Config.max_meta_object_cache_num,
+                                ignored -> listTableNames(),
+                                localTableName -> Optional.ofNullable(
+                                        buildTableForInit(null, localTableName,
+                                                
Util.genIdByName(extCatalog.getName(), name, localTableName),
+                                                extCatalog,
+                                                this, true)),
+                                (key, value, cause) -> 
value.ifPresent(ExternalTable::unsetObjectCreated));
+                    }
+                    setLastUpdateTime(System.currentTimeMillis());
+                } else {
+                    if (!Env.getCurrentEnv().isMaster()) {
+                        // Forward to master and wait the journal to replay.
+                        int waitTimeOut = ConnectContext.get() == null ? 300 : 
ConnectContext.get().getExecTimeout();
+                        MasterCatalogExecutor remoteExecutor = new 
MasterCatalogExecutor(waitTimeOut * 1000);
+                        try {
+                            remoteExecutor.forward(extCatalog.getId(), id);
+                        } catch (Exception e) {
+                            Util.logAndThrowRuntimeException(LOG,
+                                    String.format("failed to forward init 
external db %s operation to master", name),
+                                    e);
+                        }
+                        return;
                     }
-                    return;
+                    init();
                 }
-                init();
+                initialized = true;
             }
-            initialized = true;
+        } finally {
+            isInitializing = false;
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalFunctionRules.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalFunctionRules.java
new file mode 100644
index 00000000000..c88eae9a686
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/ExternalFunctionRules.java
@@ -0,0 +1,287 @@
+// 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.datasource;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.datasource.jdbc.source.JdbcFunctionPushDownRule;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gson.Gson;
+import lombok.Data;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * External push down rules for functions.
+ * This class provides a way to define which functions can be pushed down to 
external data sources.
+ * It supports both supported and unsupported functions in a JSON format.
+ */
+public class ExternalFunctionRules {
+    private static final Logger LOG = 
LogManager.getLogger(ExternalFunctionRules.class);
+
+    private FunctionPushDownRule functionPushDownRule;
+    private FunctionRewriteRules functionRewriteRules;
+
+    public static ExternalFunctionRules create(String datasource, String 
jsonRules) {
+        ExternalFunctionRules rules = new ExternalFunctionRules();
+        rules.functionPushDownRule = FunctionPushDownRule.create(datasource, 
jsonRules);
+        rules.functionRewriteRules = FunctionRewriteRules.create(datasource, 
jsonRules);
+        return rules;
+    }
+
+    public static void check(String jsonRules) throws DdlException {
+        if (Strings.isNullOrEmpty(jsonRules)) {
+            return;
+        }
+        FunctionPushDownRule.check(jsonRules);
+        FunctionRewriteRules.check(jsonRules);
+    }
+
+    public FunctionPushDownRule getFunctionPushDownRule() {
+        return functionPushDownRule;
+    }
+
+    public FunctionRewriteRules getFunctionRewriteRule() {
+        return functionRewriteRules;
+    }
+
+    /**
+     * FunctionPushDownRule is used to determine if a function can be pushed 
down
+     */
+    public static class FunctionPushDownRule {
+        private final Set<String> supportedFunctions = Sets.newHashSet();
+        private final Set<String> unsupportedFunctions = Sets.newHashSet();
+
+        public static FunctionPushDownRule create(String datasource, String 
jsonRules) {
+            FunctionPushDownRule funcRule = new FunctionPushDownRule();
+            try {
+                // Add default push down rules
+                switch (datasource.toLowerCase()) {
+                    case "mysql":
+                        
funcRule.unsupportedFunctions.addAll(JdbcFunctionPushDownRule.MYSQL_UNSUPPORTED_FUNCTIONS);
+                        break;
+                    case "clickhouse":
+                        
funcRule.supportedFunctions.addAll(JdbcFunctionPushDownRule.CLICKHOUSE_SUPPORTED_FUNCTIONS);
+                        break;
+                    case "oracle":
+                        
funcRule.supportedFunctions.addAll(JdbcFunctionPushDownRule.ORACLE_SUPPORTED_FUNCTIONS);
+                        break;
+                    default:
+                        break;
+                }
+                if (!Strings.isNullOrEmpty(jsonRules)) {
+                    // set custom rules
+                    Gson gson = new Gson();
+                    PushDownRules rules = gson.fromJson(jsonRules, 
PushDownRules.class);
+                    funcRule.setCustomRules(rules);
+                }
+                return funcRule;
+            } catch (Exception e) {
+                LOG.warn("should not happen", e);
+                return funcRule;
+            }
+        }
+
+        public static void check(String jsonRules) throws DdlException {
+            try {
+                Gson gson = new Gson();
+                PushDownRules rules = gson.fromJson(jsonRules, 
PushDownRules.class);
+                if (rules == null) {
+                    throw new DdlException("Push down rules cannot be null");
+                }
+                rules.check();
+            } catch (Exception e) {
+                throw new DdlException("Failed to parse push down rules: " + 
jsonRules, e);
+            }
+        }
+
+        private void setCustomRules(PushDownRules rules) {
+            if (rules != null && rules.getPushdown() != null) {
+                if (rules.getPushdown().getSupported() != null) {
+                    rules.getPushdown().getSupported().stream()
+                            .map(String::toLowerCase)
+                            .forEach(supportedFunctions::add);
+                }
+                if (rules.getPushdown().getUnsupported() != null) {
+                    rules.getPushdown().getUnsupported().stream()
+                            .map(String::toLowerCase)
+                            .forEach(unsupportedFunctions::add);
+                }
+            }
+        }
+
+        /**
+         * Checks if the function can be pushed down.
+         *
+         * @param functionName the name of the function to check
+         * @return true if the function can be pushed down, false otherwise
+         */
+        public boolean canPushDown(String functionName) {
+            if (supportedFunctions.isEmpty() && 
unsupportedFunctions.isEmpty()) {
+                return false;
+            }
+
+            // If supportedFunctions is not empty, only functions in 
supportedFunctions can return true
+            if (!supportedFunctions.isEmpty()) {
+                return supportedFunctions.contains(functionName.toLowerCase());
+            }
+
+            // For functions contained in unsupportedFunctions, return false
+            if (unsupportedFunctions.contains(functionName.toLowerCase())) {
+                return false;
+            }
+
+            // In other cases, return true
+            return true;
+        }
+    }
+
+    /**
+     * FunctionRewriteRule is used to rewrite function names based on provided 
rules.
+     * It allows for mapping one function name to another.
+     */
+    public static class FunctionRewriteRules {
+        private final Map<String, String> rewriteMap = Maps.newHashMap();
+
+        public static FunctionRewriteRules create(String datasource, String 
jsonRules) {
+            FunctionRewriteRules rewriteRule = new FunctionRewriteRules();
+            try {
+                // Add default rewrite rules
+                switch (datasource.toLowerCase()) {
+                    case "mysql":
+                        
rewriteRule.rewriteMap.putAll(JdbcFunctionPushDownRule.REPLACE_MYSQL_FUNCTIONS);
+                        break;
+                    case "clickhouse":
+                        
rewriteRule.rewriteMap.putAll(JdbcFunctionPushDownRule.REPLACE_CLICKHOUSE_FUNCTIONS);
+                        break;
+                    case "oracle":
+                        
rewriteRule.rewriteMap.putAll(JdbcFunctionPushDownRule.REPLACE_ORACLE_FUNCTIONS);
+                        break;
+                    default:
+                        break;
+                }
+                if (!Strings.isNullOrEmpty(jsonRules)) {
+                    // set custom rules
+                    Gson gson = new Gson();
+                    RewriteRules rules = gson.fromJson(jsonRules, 
RewriteRules.class);
+                    rewriteRule.setCustomRules(rules);
+                }
+                return rewriteRule;
+            } catch (Exception e) {
+                LOG.warn("should not happen", e);
+                return rewriteRule;
+            }
+        }
+
+        private void setCustomRules(RewriteRules rules) {
+            if (rules != null && rules.getRewrite() != null) {
+                this.rewriteMap.putAll(rules.getRewrite());
+            }
+        }
+
+        public String rewriteFunction(String origFuncName) {
+            return rewriteMap.getOrDefault(origFuncName, origFuncName);
+        }
+
+        public static void check(String jsonRules) throws DdlException {
+            try {
+                Gson gson = new Gson();
+                RewriteRules rules = gson.fromJson(jsonRules, 
RewriteRules.class);
+                if (rules == null) {
+                    throw new DdlException("Rewrite rules cannot be null");
+                }
+                rules.check();
+            } catch (Exception e) {
+                throw new DdlException("Failed to parse rewrite rules: " + 
jsonRules, e);
+            }
+        }
+    }
+
+    /**
+     * push down rules in json format.
+     * eg:
+     * {
+     * "pushdown": {
+     * "supported": ["function1", "function2"],
+     * "unsupported": ["function3", "function4"]
+     * }
+     * }
+     */
+    @Data
+    public static class PushDownRules {
+        private PushDown pushdown;
+
+        @Data
+        public static class PushDown {
+            private List<String> supported;
+            private List<String> unsupported;
+        }
+
+        public void check() {
+            if (pushdown != null) {
+                if (pushdown.getSupported() != null) {
+                    for (String func : pushdown.getSupported()) {
+                        if (Strings.isNullOrEmpty(func)) {
+                            throw new IllegalArgumentException("Supported 
function name cannot be empty");
+                        }
+                    }
+                }
+                if (pushdown.getUnsupported() != null) {
+                    for (String func : pushdown.getUnsupported()) {
+                        if (Strings.isNullOrEmpty(func)) {
+                            throw new IllegalArgumentException("Unsupported 
function name cannot be empty");
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * push down rules in json format.
+     * eg:
+     * {
+     * "rewrite": {
+     * "func1": "func2",
+     * "func3": "func4"
+     * }
+     * }
+     */
+    @Data
+    public static class RewriteRules {
+        private Map<String, String> rewrite;
+
+        public void check() {
+            if (rewrite != null) {
+                for (Map.Entry<String, String> entry : rewrite.entrySet()) {
+                    String origFunc = entry.getKey();
+                    String newFunc = entry.getValue();
+                    if (Strings.isNullOrEmpty(origFunc) || 
Strings.isNullOrEmpty(newFunc)) {
+                        throw new IllegalArgumentException("Function names in 
rewrite rules cannot be empty");
+                    }
+                }
+            }
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
index 5094207490c..4899cb2ec1c 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalCatalog.java
@@ -28,6 +28,7 @@ import org.apache.doris.common.FeConstants;
 import org.apache.doris.datasource.CatalogProperty;
 import org.apache.doris.datasource.ExternalCatalog;
 import org.apache.doris.datasource.ExternalDatabase;
+import org.apache.doris.datasource.ExternalFunctionRules;
 import org.apache.doris.datasource.ExternalTable;
 import org.apache.doris.datasource.InitCatalogLog;
 import org.apache.doris.datasource.SessionContext;
@@ -77,6 +78,7 @@ public class JdbcExternalCatalog extends ExternalCatalog {
     // or Gson will throw exception with HikariCP
     private transient JdbcClient jdbcClient;
     private IdentifierMapping identifierMapping;
+    private ExternalFunctionRules functionRules;
 
     public JdbcExternalCatalog(long catalogId, String name, String resource, 
Map<String, String> props,
             String comment)
@@ -107,6 +109,9 @@ public class JdbcExternalCatalog extends ExternalCatalog {
                 getExcludeDatabaseMap());
         JdbcResource.checkConnectionPoolProperties(getConnectionPoolMinSize(), 
getConnectionPoolMaxSize(),
                 getConnectionPoolMaxWaitTime(), 
getConnectionPoolMaxLifeTime());
+
+        // check function rules
+        
ExternalFunctionRules.check(catalogProperty.getProperties().getOrDefault(JdbcResource.FUNCTION_RULES,
 ""));
     }
 
     @Override
@@ -223,6 +228,8 @@ public class JdbcExternalCatalog extends ExternalCatalog {
     @Override
     protected void initLocalObjectsImpl() {
         jdbcClient = createJdbcClient();
+        this.functionRules = 
ExternalFunctionRules.create(jdbcClient.getDbType(),
+                catalogProperty.getOrDefault(JdbcResource.FUNCTION_RULES, ""));
     }
 
     private JdbcClient createJdbcClient() {
@@ -437,4 +444,8 @@ public class JdbcExternalCatalog extends ExternalCatalog {
 
         return testTable;
     }
+
+    public ExternalFunctionRules getFunctionRules() {
+        return functionRules;
+    }
 }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalTable.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalTable.java
index b3ff728bb7d..2bcd2277251 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalTable.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/JdbcExternalTable.java
@@ -187,6 +187,7 @@ public class JdbcExternalTable extends ExternalTable {
             remoteColumnNames.put(column.getName(), remoteColumnName);
         }
         jdbcTable.setRemoteColumnNames(remoteColumnNames);
+        jdbcTable.setExternalFunctionRules(jdbcCatalog.getFunctionRules());
 
         return jdbcTable;
     }
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcFunctionPushDownRule.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcFunctionPushDownRule.java
index a765681c402..48aac798cf0 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcFunctionPushDownRule.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcFunctionPushDownRule.java
@@ -23,6 +23,8 @@ import org.apache.doris.analysis.FunctionName;
 import org.apache.doris.analysis.TimestampArithmeticExpr;
 import org.apache.doris.catalog.TableIf.TableType;
 import org.apache.doris.common.Config;
+import org.apache.doris.datasource.ExternalFunctionRules;
+import org.apache.doris.datasource.ExternalFunctionRules.FunctionRewriteRules;
 import org.apache.doris.thrift.TOdbcTableType;
 
 import com.google.common.base.Preconditions;
@@ -34,11 +36,10 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeSet;
-import java.util.function.Predicate;
 
 public class JdbcFunctionPushDownRule {
     private static final Logger LOG = 
LogManager.getLogger(JdbcFunctionPushDownRule.class);
-    private static final TreeSet<String> MYSQL_UNSUPPORTED_FUNCTIONS = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+    public static final TreeSet<String> MYSQL_UNSUPPORTED_FUNCTIONS = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
 
     static {
         MYSQL_UNSUPPORTED_FUNCTIONS.add("date_trunc");
@@ -47,14 +48,14 @@ public class JdbcFunctionPushDownRule {
         
MYSQL_UNSUPPORTED_FUNCTIONS.addAll(Arrays.asList(Config.jdbc_mysql_unsupported_pushdown_functions));
     }
 
-    private static final TreeSet<String> CLICKHOUSE_SUPPORTED_FUNCTIONS = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+    public static final TreeSet<String> CLICKHOUSE_SUPPORTED_FUNCTIONS = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
 
     static {
         CLICKHOUSE_SUPPORTED_FUNCTIONS.add("from_unixtime");
         CLICKHOUSE_SUPPORTED_FUNCTIONS.add("unix_timestamp");
     }
 
-    private static final TreeSet<String> ORACLE_SUPPORTED_FUNCTIONS = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+    public static final TreeSet<String> ORACLE_SUPPORTED_FUNCTIONS = new 
TreeSet<>(String.CASE_INSENSITIVE_ORDER);
 
     static {
         ORACLE_SUPPORTED_FUNCTIONS.add("nvl");
@@ -73,21 +74,21 @@ public class JdbcFunctionPushDownRule {
         return 
!ORACLE_SUPPORTED_FUNCTIONS.contains(functionName.toLowerCase());
     }
 
-    private static final Map<String, String> REPLACE_MYSQL_FUNCTIONS = 
Maps.newHashMap();
+    public static final Map<String, String> REPLACE_MYSQL_FUNCTIONS = 
Maps.newHashMap();
 
     static {
         REPLACE_MYSQL_FUNCTIONS.put("nvl", "ifnull");
         REPLACE_MYSQL_FUNCTIONS.put("to_date", "date");
     }
 
-    private static final Map<String, String> REPLACE_CLICKHOUSE_FUNCTIONS = 
Maps.newHashMap();
+    public static final Map<String, String> REPLACE_CLICKHOUSE_FUNCTIONS = 
Maps.newHashMap();
 
     static {
         REPLACE_CLICKHOUSE_FUNCTIONS.put("from_unixtime", "FROM_UNIXTIME");
         REPLACE_CLICKHOUSE_FUNCTIONS.put("unix_timestamp", "toUnixTimestamp");
     }
 
-    private static final Map<String, String> REPLACE_ORACLE_FUNCTIONS = 
Maps.newHashMap();
+    public static final Map<String, String> REPLACE_ORACLE_FUNCTIONS = 
Maps.newHashMap();
 
     static {
         REPLACE_ORACLE_FUNCTIONS.put("ifnull", "nvl");
@@ -105,77 +106,54 @@ public class JdbcFunctionPushDownRule {
         return 
REPLACE_ORACLE_FUNCTIONS.containsKey(functionName.toLowerCase());
     }
 
-    public static Expr processFunctions(TOdbcTableType tableType, Expr expr, 
List<String> errors) {
-        if (tableType == null || expr == null) {
+    public static Expr processFunctions(TOdbcTableType tableType, Expr expr, 
List<String> errors,
+            ExternalFunctionRules functionRules) {
+        if (tableType == null || expr == null || functionRules == null) {
             return expr;
         }
 
-        Predicate<String> checkFunction;
-        Predicate<String> replaceFunction;
-
-        if (TOdbcTableType.MYSQL.equals(tableType)) {
-            replaceFunction = 
JdbcFunctionPushDownRule::isReplaceMysqlFunctions;
-            checkFunction = 
JdbcFunctionPushDownRule::isMySQLFunctionUnsupported;
-        } else if (TOdbcTableType.CLICKHOUSE.equals(tableType)) {
-            replaceFunction = 
JdbcFunctionPushDownRule::isReplaceClickHouseFunctions;
-            checkFunction = 
JdbcFunctionPushDownRule::isClickHouseFunctionUnsupported;
-        } else if (TOdbcTableType.ORACLE.equals(tableType)) {
-            replaceFunction = 
JdbcFunctionPushDownRule::isReplaceOracleFunctions;
-            checkFunction = 
JdbcFunctionPushDownRule::isOracleFunctionUnsupported;
-        } else {
-            return expr;
-        }
-
-        return processFunctionsRecursively(expr, checkFunction, 
replaceFunction, errors, tableType);
+        return processFunctionsRecursively(expr, functionRules, errors, 
tableType);
     }
 
-    private static Expr processFunctionsRecursively(Expr expr, 
Predicate<String> checkFunction,
-            Predicate<String> replaceFunction, List<String> errors, 
TOdbcTableType tableType) {
+    private static Expr processFunctionsRecursively(Expr expr, 
ExternalFunctionRules functionRules, List<String> errors,
+            TOdbcTableType tableType) {
         if (expr instanceof FunctionCallExpr) {
             FunctionCallExpr functionCallExpr = (FunctionCallExpr) expr;
             String func = functionCallExpr.getFnName().getFunction();
 
             Preconditions.checkArgument(!func.isEmpty(), "function can not be 
empty");
 
-            if (checkFunction.test(func)) {
-                String errMsg = "Unsupported function: " + func + " in expr: " 
+ expr.toExternalSql(
-                        TableType.JDBC_EXTERNAL_TABLE, null)
-                        + " in JDBC Table Type: " + tableType;
-                LOG.warn(errMsg);
-                errors.add(errMsg);
+            // 1. check can push down
+            if (!functionRules.getFunctionPushDownRule().canPushDown(func)) {
+                if (LOG.isDebugEnabled()) {
+                    String errMsg = "Unsupported function: " + func + " in 
expr: " + expr.toExternalSql(
+                            TableType.JDBC_EXTERNAL_TABLE, null)
+                            + " in JDBC Table Type: " + tableType;
+                    LOG.debug(errMsg);
+                }
+                errors.add("has error");
             }
 
-            replaceFunctionNameIfNecessary(func, replaceFunction, 
functionCallExpr, tableType);
-
+            // 2. replace function
+            replaceFunctionNameIfNecessary(func, 
functionRules.getFunctionRewriteRule(), functionCallExpr);
             expr = replaceGenericFunctionExpr(functionCallExpr, func);
         }
 
         List<Expr> children = expr.getChildren();
         for (int i = 0; i < children.size(); i++) {
             Expr child = children.get(i);
-            Expr newChild = processFunctionsRecursively(child, checkFunction, 
replaceFunction, errors, tableType);
+            Expr newChild = processFunctionsRecursively(child, functionRules, 
errors, tableType);
             expr.setChild(i, newChild);
         }
 
         return expr;
     }
 
-    private static void replaceFunctionNameIfNecessary(String func, 
Predicate<String> replaceFunction,
-            FunctionCallExpr functionCallExpr, TOdbcTableType tableType) {
-        if (replaceFunction.test(func)) {
-            String newFunc;
-            if (TOdbcTableType.MYSQL.equals(tableType)) {
-                newFunc = REPLACE_MYSQL_FUNCTIONS.get(func.toLowerCase());
-            } else if (TOdbcTableType.CLICKHOUSE.equals(tableType)) {
-                newFunc = REPLACE_CLICKHOUSE_FUNCTIONS.get(func);
-            } else if (TOdbcTableType.ORACLE.equals(tableType)) {
-                newFunc = REPLACE_ORACLE_FUNCTIONS.get(func);
-            } else {
-                newFunc = null;
-            }
-            if (newFunc != null) {
-                
functionCallExpr.setFnName(FunctionName.createBuiltinName(newFunc));
-            }
+    private static void replaceFunctionNameIfNecessary(String func, 
FunctionRewriteRules rewriteRule,
+            FunctionCallExpr functionCallExpr) {
+        String newFuncName = rewriteRule.rewriteFunction(func);
+        if (!newFuncName.equals(func)) {
+            
functionCallExpr.setFnName(FunctionName.createBuiltinName(newFuncName));
         }
     }
 
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcScanNode.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcScanNode.java
index 31026e6b877..019cceca6a7 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcScanNode.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/jdbc/source/JdbcScanNode.java
@@ -39,6 +39,7 @@ import org.apache.doris.catalog.TableIf;
 import org.apache.doris.catalog.TableIf.TableType;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.ExternalFunctionRules;
 import org.apache.doris.datasource.ExternalScanNode;
 import org.apache.doris.datasource.jdbc.JdbcExternalTable;
 import org.apache.doris.planner.PlanNodeId;
@@ -131,7 +132,8 @@ public class JdbcScanNode extends ExternalScanNode {
 
         ArrayList<Expr> conjunctsList = Expr.cloneList(conjuncts, sMap);
         List<String> errors = Lists.newArrayList();
-        List<Expr> pushDownConjuncts = 
collectConjunctsToPushDown(conjunctsList, errors);
+        List<Expr> pushDownConjuncts = 
collectConjunctsToPushDown(conjunctsList, errors,
+                tbl.getExternalFunctionRules());
 
         for (Expr individualConjunct : pushDownConjuncts) {
             String filter = conjunctExprToString(jdbcType, individualConjunct, 
tbl);
@@ -140,13 +142,15 @@ public class JdbcScanNode extends ExternalScanNode {
         }
     }
 
-    private List<Expr> collectConjunctsToPushDown(List<Expr> conjunctsList, 
List<String> errors) {
+    private List<Expr> collectConjunctsToPushDown(List<Expr> conjunctsList, 
List<String> errors,
+            ExternalFunctionRules functionRules) {
         List<Expr> pushDownConjuncts = new ArrayList<>();
         for (Expr p : conjunctsList) {
             if (shouldPushDownConjunct(jdbcType, p)) {
                 List<Expr> individualConjuncts = p.getConjuncts();
                 for (Expr individualConjunct : individualConjuncts) {
-                    Expr newp = 
JdbcFunctionPushDownRule.processFunctions(jdbcType, individualConjunct, errors);
+                    Expr newp = 
JdbcFunctionPushDownRule.processFunctions(jdbcType, individualConjunct, errors,
+                            functionRules);
                     if (!errors.isEmpty()) {
                         errors.clear();
                         continue;
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalFunctionPushDownRulesTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalFunctionPushDownRulesTest.java
new file mode 100644
index 00000000000..5f8591ed1d4
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalFunctionPushDownRulesTest.java
@@ -0,0 +1,431 @@
+// 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.datasource;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.datasource.ExternalFunctionRules.FunctionPushDownRule;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class ExternalFunctionPushDownRulesTest {
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithMysqlDataSource() {
+        // Test MySQL datasource with default rules
+        FunctionPushDownRule rule = FunctionPushDownRule.create("mysql", null);
+
+        // MySQL has unsupported functions by default, supported functions is 
empty
+        Assertions.assertFalse(rule.canPushDown("date_trunc"));
+        Assertions.assertFalse(rule.canPushDown("money_format"));
+        Assertions.assertFalse(rule.canPushDown("negative"));
+
+        // Test case insensitivity
+        Assertions.assertFalse(rule.canPushDown("DATE_TRUNC"));
+        Assertions.assertFalse(rule.canPushDown("Money_Format"));
+
+        // Functions not in unsupported list should be allowed (since 
supportedFunctions is empty)
+        Assertions.assertTrue(rule.canPushDown("sum"));
+        Assertions.assertTrue(rule.canPushDown("count"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithClickHouseDataSource() {
+        // Test ClickHouse datasource with default rules
+        FunctionPushDownRule rule = FunctionPushDownRule.create("clickhouse", 
null);
+
+        // ClickHouse has supported functions by default, so only supported 
functions return true
+        Assertions.assertTrue(rule.canPushDown("from_unixtime"));
+        Assertions.assertTrue(rule.canPushDown("unix_timestamp"));
+
+        // Test case insensitivity
+        Assertions.assertTrue(rule.canPushDown("FROM_UNIXTIME"));
+        Assertions.assertTrue(rule.canPushDown("Unix_Timestamp"));
+
+        // Functions not in supported list should be denied (since 
supportedFunctions is not empty)
+        Assertions.assertFalse(rule.canPushDown("unknown_function"));
+        Assertions.assertFalse(rule.canPushDown("custom_func"));
+        Assertions.assertFalse(rule.canPushDown("sum"));
+        Assertions.assertFalse(rule.canPushDown("count"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithOracleDataSource() {
+        // Test Oracle datasource with default rules
+        FunctionPushDownRule rule = FunctionPushDownRule.create("oracle", 
null);
+
+        // Oracle has supported functions by default, so only supported 
functions return true
+        Assertions.assertTrue(rule.canPushDown("nvl"));
+        Assertions.assertTrue(rule.canPushDown("ifnull"));
+
+        // Test case insensitivity
+        Assertions.assertTrue(rule.canPushDown("NVL"));
+        Assertions.assertTrue(rule.canPushDown("IfNull"));
+
+        // Functions not in supported list should be denied (since 
supportedFunctions is not empty)
+        Assertions.assertFalse(rule.canPushDown("unknown_function"));
+        Assertions.assertFalse(rule.canPushDown("custom_func"));
+        Assertions.assertFalse(rule.canPushDown("sum"));
+        Assertions.assertFalse(rule.canPushDown("count"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithUnknownDataSource() {
+        // Test unknown datasource should have no default rules
+        FunctionPushDownRule rule = FunctionPushDownRule.create("unknown", 
null);
+
+        // With no rules, all functions should be denied
+        Assertions.assertFalse(rule.canPushDown("any_function"));
+        Assertions.assertFalse(rule.canPushDown("sum"));
+        Assertions.assertFalse(rule.canPushDown("count"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithValidCustomRules() {
+        // Test custom rules with supported functions
+        String jsonRules = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"custom_func1\", \"Custom_Func2\"],\n"
+                + "    \"unsupported\": [\"blocked_func1\", 
\"Blocked_Func2\"]\n"
+                + "  }\n"
+                + "}";
+
+        FunctionPushDownRule rule = FunctionPushDownRule.create("mysql", 
jsonRules);
+
+        // Since supportedFunctions is not empty, only supported functions 
return true
+        Assertions.assertTrue(rule.canPushDown("custom_func1"));
+        Assertions.assertTrue(rule.canPushDown("custom_func2")); // case 
insensitive
+        Assertions.assertTrue(rule.canPushDown("CUSTOM_FUNC1"));
+
+        // Functions not in supported list should be denied (even if not in 
unsupported list)
+        Assertions.assertFalse(rule.canPushDown("other_function"));
+        Assertions.assertFalse(rule.canPushDown("sum"));
+        Assertions.assertFalse(rule.canPushDown("count"));
+
+        // Functions in unsupported list should still be denied
+        Assertions.assertFalse(rule.canPushDown("blocked_func1"));
+        Assertions.assertFalse(rule.canPushDown("blocked_func2")); // case 
insensitive
+        Assertions.assertFalse(rule.canPushDown("BLOCKED_FUNC1"));
+
+        // Default MySQL unsupported functions should be denied
+        Assertions.assertFalse(rule.canPushDown("date_trunc"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithOnlySupportedCustomRules() {
+        // Test custom rules with only supported functions
+        String jsonRules = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"allowed_func1\", \"allowed_func2\"]\n"
+                + "  }\n"
+                + "}";
+
+        FunctionPushDownRule rule = FunctionPushDownRule.create("unknown", 
jsonRules);
+
+        // Since supportedFunctions is not empty, only supported functions 
return true
+        Assertions.assertTrue(rule.canPushDown("allowed_func1"));
+        Assertions.assertTrue(rule.canPushDown("allowed_func2"));
+
+        // Other functions should be denied (since supportedFunctions is not 
empty)
+        Assertions.assertFalse(rule.canPushDown("other_function"));
+        Assertions.assertFalse(rule.canPushDown("sum"));
+        Assertions.assertFalse(rule.canPushDown("count"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithOnlyUnsupportedCustomRules() 
{
+        // Test custom rules with only unsupported functions
+        String jsonRules = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"unsupported\": [\"blocked_func1\", 
\"blocked_func2\"]\n"
+                + "  }\n"
+                + "}";
+
+        FunctionPushDownRule rule = FunctionPushDownRule.create("unknown", 
jsonRules);
+
+        // Custom unsupported functions should be denied
+        Assertions.assertFalse(rule.canPushDown("blocked_func1"));
+        Assertions.assertFalse(rule.canPushDown("blocked_func2"));
+
+        // Other functions should be allowed (default behavior)
+        Assertions.assertTrue(rule.canPushDown("other_function"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithEmptyCustomRules() {
+        // Test empty custom rules
+        String jsonRules = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [],\n"
+                + "    \"unsupported\": []\n"
+                + "  }\n"
+                + "}";
+
+        FunctionPushDownRule rule = FunctionPushDownRule.create("unknown", 
jsonRules);
+
+        // With empty rules, all functions should be denied
+        Assertions.assertFalse(rule.canPushDown("any_function"));
+        Assertions.assertFalse(rule.canPushDown("sum"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithNullCustomRules() {
+        // Test null pushdown section
+        String jsonRules = "{\n"
+                + "  \"pushdown\": null\n"
+                + "}";
+
+        FunctionPushDownRule rule = FunctionPushDownRule.create("unknown", 
jsonRules);
+
+        // With null rules, all functions should be denied
+        Assertions.assertFalse(rule.canPushDown("any_function"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithInvalidJson() {
+        // Test invalid JSON should not throw exception but return default rule
+        String invalidJson = "{ invalid json }";
+
+        FunctionPushDownRule rule = FunctionPushDownRule.create("unknown", 
invalidJson);
+
+        // Should return a rule with no functions configured
+        Assertions.assertFalse(rule.canPushDown("any_function"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateWithEmptyJsonRules() {
+        // Test empty string rules
+        FunctionPushDownRule rule = FunctionPushDownRule.create("mysql", "");
+
+        // Should only have default MySQL rules
+        Assertions.assertFalse(rule.canPushDown("date_trunc"));
+        Assertions.assertTrue(rule.canPushDown("other_function"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCreateCaseInsensitiveDataSource() {
+        // Test case insensitivity for datasource names
+        FunctionPushDownRule mysqlRule = FunctionPushDownRule.create("MYSQL", 
null);
+        FunctionPushDownRule clickhouseRule = 
FunctionPushDownRule.create("ClickHouse", null);
+        FunctionPushDownRule oracleRule = 
FunctionPushDownRule.create("Oracle", null);
+
+        // Should apply correct default rules regardless of case
+        Assertions.assertFalse(mysqlRule.canPushDown("date_trunc"));
+        Assertions.assertTrue(clickhouseRule.canPushDown("from_unixtime"));
+        Assertions.assertTrue(oracleRule.canPushDown("nvl"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCanPushDownLogic() {
+        // Test the canPushDown logic with different scenarios
+
+        // 1. Both supported and unsupported are empty -> return false
+        FunctionPushDownRule emptyRule = 
FunctionPushDownRule.create("unknown", null);
+        Assertions.assertFalse(emptyRule.canPushDown("any_function"));
+
+        // 2. Function in supported list -> return true (only when 
supportedFunctions is not empty)
+        String supportedJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"func1\"]\n"
+                + "  }\n"
+                + "}";
+        FunctionPushDownRule supportedRule = 
FunctionPushDownRule.create("unknown", supportedJson);
+        Assertions.assertTrue(supportedRule.canPushDown("func1"));
+
+        // 3. Function not in supported list when supportedFunctions is not 
empty -> return false
+        Assertions.assertFalse(supportedRule.canPushDown("other_func"));
+
+        // 4. Function in unsupported list -> return false
+        String unsupportedJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"unsupported\": [\"func1\"]\n"
+                + "  }\n"
+                + "}";
+        FunctionPushDownRule unsupportedRule = 
FunctionPushDownRule.create("unknown", unsupportedJson);
+        Assertions.assertFalse(unsupportedRule.canPushDown("func1"));
+
+        // 5. Function not in unsupported list when supportedFunctions is 
empty -> return true
+        Assertions.assertTrue(unsupportedRule.canPushDown("other_func"));
+
+        // 6. Priority test: when supportedFunctions is not empty, only 
supported functions return true
+        String bothJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"func1\"],\n"
+                + "    \"unsupported\": [\"func2\"]\n"
+                + "  }\n"
+                + "}";
+        FunctionPushDownRule bothRule = FunctionPushDownRule.create("unknown", 
bothJson);
+        Assertions.assertTrue(bothRule.canPushDown("func1")); // in supported 
list
+        Assertions.assertFalse(bothRule.canPushDown("func2")); // not in 
supported list (even though in unsupported)
+        Assertions.assertFalse(bothRule.canPushDown("func3")); // not in 
supported list
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCheck() throws DdlException {
+        // Test valid JSON rules
+        String validJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"func1\", \"func2\"],\n"
+                + "    \"unsupported\": [\"func3\", \"func4\"]\n"
+                + "  }\n"
+                + "}";
+
+        // Should not throw exception
+        Assertions.assertDoesNotThrow(() -> {
+            FunctionPushDownRule.check(validJson);
+        });
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCheckWithInvalidJson() {
+        // Test invalid JSON rules
+        String invalidJson = "{ invalid json }";
+
+        // Should throw DdlException
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionPushDownRule.check(invalidJson);
+        });
+
+        Assertions.assertTrue(exception.getMessage().contains("Failed to parse 
push down rules"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCheckWithEmptyJson() throws 
DdlException {
+        // Test empty JSON
+        String emptyJson = "{}";
+
+        // Should not throw exception
+        Assertions.assertDoesNotThrow(() -> {
+            FunctionPushDownRule.check(emptyJson);
+        });
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCheckWithNullJson() throws 
DdlException {
+        // Test null JSON
+        String nullJson = null;
+
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionPushDownRule.check(nullJson);
+        });
+        Assertions.assertTrue(exception.getMessage().contains("Failed to parse 
push down rules"));
+    }
+
+    @Test
+    public void testFunctionPushDownRuleCheckWithMalformedJson() {
+        // Test various malformed JSON strings
+        String[] malformedJsons = {
+                "{ \"pushdown\": }",                    // Missing value
+                "{ \"pushdown\": { \"supported\": } }", // Missing array
+                "{ \"pushdown\" { \"supported\": [] } }", // Missing colon
+                "{ \"pushdown\": { \"supported\": [\"func1\",] } }", // 
Trailing comma
+                "{ \"pushdown\": { \"supported\": [\"func1\" \"func2\"] } }" 
// Missing comma
+        };
+
+        for (String malformedJson : malformedJsons) {
+            DdlException exception = 
Assertions.assertThrows(DdlException.class, () -> {
+                FunctionPushDownRule.check(malformedJson);
+            });
+
+            Assertions.assertTrue(exception.getMessage().contains("Failed to 
parse push down rules"));
+        }
+    }
+
+    @Test
+    public void testFunctionPushDownRuleWithComplexCustomRules() {
+        // Test complex custom rules that override and extend default rules
+        String complexJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"date_trunc\", \"custom_func\"],\n"
+                + "    \"unsupported\": [\"from_unixtime\", 
\"another_func\"]\n"
+                + "  }\n"
+                + "}";
+
+        // Test with MySQL (has default unsupported functions)
+        FunctionPushDownRule mysqlRule = FunctionPushDownRule.create("mysql", 
complexJson);
+
+        // Since supportedFunctions is not empty, only supported functions 
return true
+        Assertions.assertTrue(mysqlRule.canPushDown("date_trunc")); // in 
supported list
+        Assertions.assertTrue(mysqlRule.canPushDown("custom_func")); // in 
supported list
+
+        // Functions not in supported list should be denied
+        Assertions.assertFalse(mysqlRule.canPushDown("from_unixtime")); // not 
in supported list
+        Assertions.assertFalse(mysqlRule.canPushDown("another_func")); // not 
in supported list
+        Assertions.assertFalse(mysqlRule.canPushDown("money_format")); // not 
in supported list
+        Assertions.assertFalse(mysqlRule.canPushDown("sum")); // not in 
supported list
+        Assertions.assertFalse(mysqlRule.canPushDown("count")); // not in 
supported list
+    }
+
+    @Test
+    public void testFunctionPushDownRuleNewLogicCases() {
+        // Additional test cases for the new logic
+
+        // Test case 1: Only unsupported functions defined (supportedFunctions 
is empty)
+        String onlyUnsupportedJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"unsupported\": [\"blocked_func\"]\n"
+                + "  }\n"
+                + "}";
+        FunctionPushDownRule onlyUnsupportedRule = 
FunctionPushDownRule.create("unknown", onlyUnsupportedJson);
+
+        // Functions in unsupported list should be denied
+        
Assertions.assertFalse(onlyUnsupportedRule.canPushDown("blocked_func"));
+
+        // Other functions should be allowed (since supportedFunctions is 
empty)
+        Assertions.assertTrue(onlyUnsupportedRule.canPushDown("allowed_func"));
+        Assertions.assertTrue(onlyUnsupportedRule.canPushDown("sum"));
+
+        // Test case 2: Both supported and unsupported functions defined
+        String bothListsJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"func1\", \"func2\"],\n"
+                + "    \"unsupported\": [\"func3\", \"func4\"]\n"
+                + "  }\n"
+                + "}";
+        FunctionPushDownRule bothListsRule = 
FunctionPushDownRule.create("unknown", bothListsJson);
+
+        // Only supported functions return true
+        Assertions.assertTrue(bothListsRule.canPushDown("func1"));
+        Assertions.assertTrue(bothListsRule.canPushDown("func2"));
+
+        // Functions in unsupported list should be denied (not in supported 
list)
+        Assertions.assertFalse(bothListsRule.canPushDown("func3"));
+        Assertions.assertFalse(bothListsRule.canPushDown("func4"));
+
+        // Other functions should be denied (not in supported list)
+        Assertions.assertFalse(bothListsRule.canPushDown("func5"));
+        Assertions.assertFalse(bothListsRule.canPushDown("other_func"));
+
+        // Test case 3: MySQL with custom supported functions
+        String mysqlSupportedJson = "{\n"
+                + "  \"pushdown\": {\n"
+                + "    \"supported\": [\"date_trunc\", \"money_format\"]\n"
+                + "  }\n"
+                + "}";
+        FunctionPushDownRule mysqlSupportedRule = 
FunctionPushDownRule.create("mysql", mysqlSupportedJson);
+
+        // Only supported functions return true (overrides default MySQL 
unsupported functions)
+        Assertions.assertTrue(mysqlSupportedRule.canPushDown("date_trunc"));
+        Assertions.assertTrue(mysqlSupportedRule.canPushDown("money_format"));
+
+        // Other functions should be denied
+        Assertions.assertFalse(mysqlSupportedRule.canPushDown("negative"));
+        Assertions.assertFalse(mysqlSupportedRule.canPushDown("sum"));
+        Assertions.assertFalse(mysqlSupportedRule.canPushDown("count"));
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalFunctionRewriteRulesTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalFunctionRewriteRulesTest.java
new file mode 100644
index 00000000000..3bca159575a
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/ExternalFunctionRewriteRulesTest.java
@@ -0,0 +1,443 @@
+// 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.datasource;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.datasource.ExternalFunctionRules.FunctionRewriteRules;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class ExternalFunctionRewriteRulesTest {
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithMysqlDataSource() {
+        // Test MySQL datasource with default rewrite rules
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", null);
+
+        // MySQL has default rewrite rules
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+
+        // Test case sensitivity - original function names should be used as-is
+        Assertions.assertEquals("NVL", rule.rewriteFunction("NVL"));
+        Assertions.assertEquals("To_Date", rule.rewriteFunction("To_Date"));
+
+        // Functions not in rewrite map should return original name
+        Assertions.assertEquals("sum", rule.rewriteFunction("sum"));
+        Assertions.assertEquals("count", rule.rewriteFunction("count"));
+        Assertions.assertEquals("unknown_func", 
rule.rewriteFunction("unknown_func"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithClickHouseDataSource() {
+        // Test ClickHouse datasource with default rewrite rules
+        FunctionRewriteRules rule = FunctionRewriteRules.create("clickhouse", 
null);
+
+        // ClickHouse has default rewrite rules
+        Assertions.assertEquals("FROM_UNIXTIME", 
rule.rewriteFunction("from_unixtime"));
+        Assertions.assertEquals("toUnixTimestamp", 
rule.rewriteFunction("unix_timestamp"));
+
+        // Test case sensitivity
+        Assertions.assertEquals("FROM_UNIXTIME", 
rule.rewriteFunction("FROM_UNIXTIME"));
+        Assertions.assertEquals("Unix_Timestamp", 
rule.rewriteFunction("Unix_Timestamp"));
+
+        // Functions not in rewrite map should return original name
+        Assertions.assertEquals("sum", rule.rewriteFunction("sum"));
+        Assertions.assertEquals("count", rule.rewriteFunction("count"));
+        Assertions.assertEquals("unknown_func", 
rule.rewriteFunction("unknown_func"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithOracleDataSource() {
+        // Test Oracle datasource with default rewrite rules
+        FunctionRewriteRules rule = FunctionRewriteRules.create("oracle", 
null);
+
+        // Oracle has default rewrite rules
+        Assertions.assertEquals("nvl", rule.rewriteFunction("ifnull"));
+
+        // Test case sensitivity
+        Assertions.assertEquals("IFNULL", rule.rewriteFunction("IFNULL"));
+        Assertions.assertEquals("IfNull", rule.rewriteFunction("IfNull"));
+
+        // Functions not in rewrite map should return original name
+        Assertions.assertEquals("sum", rule.rewriteFunction("sum"));
+        Assertions.assertEquals("count", rule.rewriteFunction("count"));
+        Assertions.assertEquals("unknown_func", 
rule.rewriteFunction("unknown_func"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithUnknownDataSource() {
+        // Test unknown datasource should have no default rewrite rules
+        FunctionRewriteRules rule = FunctionRewriteRules.create("unknown", 
null);
+
+        // All functions should return original name (no rewrite rules)
+        Assertions.assertEquals("any_function", 
rule.rewriteFunction("any_function"));
+        Assertions.assertEquals("sum", rule.rewriteFunction("sum"));
+        Assertions.assertEquals("count", rule.rewriteFunction("count"));
+        Assertions.assertEquals("nvl", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("ifnull"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithValidCustomRules() {
+        // Test custom rewrite rules
+        String jsonRules = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"old_func1\": \"new_func1\",\n"
+                + "    \"Old_Func2\": \"New_Func2\",\n"
+                + "    \"CUSTOM_FUNC\": \"custom_replacement\"\n"
+                + "  }\n"
+                + "}";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", 
jsonRules);
+
+        // Custom rewrite rules should work
+        Assertions.assertEquals("new_func1", 
rule.rewriteFunction("old_func1"));
+        Assertions.assertEquals("New_Func2", 
rule.rewriteFunction("Old_Func2"));
+        Assertions.assertEquals("custom_replacement", 
rule.rewriteFunction("CUSTOM_FUNC"));
+
+        // Default MySQL rewrite rules should still work
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+
+        // Functions not in any rewrite map should return original name
+        Assertions.assertEquals("other_function", 
rule.rewriteFunction("other_function"));
+        Assertions.assertEquals("sum", rule.rewriteFunction("sum"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithEmptyCustomRules() {
+        // Test empty custom rewrite rules
+        String jsonRules = "{\n"
+                + "  \"rewrite\": {}\n"
+                + "}";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", 
jsonRules);
+
+        // Default MySQL rewrite rules should still work
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+
+        // Other functions should return original name
+        Assertions.assertEquals("other_function", 
rule.rewriteFunction("other_function"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithNullCustomRules() {
+        // Test null rewrite section
+        String jsonRules = "{\n"
+                + "  \"rewrite\": null\n"
+                + "}";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", 
jsonRules);
+
+        // Default MySQL rewrite rules should still work
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+
+        // Other functions should return original name
+        Assertions.assertEquals("other_function", 
rule.rewriteFunction("other_function"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithNullRewriteMap() {
+        // Test null rewrite map - this test is no longer relevant with the 
new format
+        // Since rewrite is now directly the map, we just test with null 
rewrite
+        String jsonRules = "{\n"
+                + "  \"rewrite\": null\n"
+                + "}";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", 
jsonRules);
+
+        // Default MySQL rewrite rules should still work
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+
+        // Other functions should return original name
+        Assertions.assertEquals("other_function", 
rule.rewriteFunction("other_function"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithInvalidJson() {
+        // Test invalid JSON should not throw exception but return default rule
+        String invalidJson = "{ invalid json }";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", 
invalidJson);
+
+        // Should still have default MySQL rewrite rules
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+
+        // Other functions should return original name
+        Assertions.assertEquals("other_function", 
rule.rewriteFunction("other_function"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithEmptyJsonRules() {
+        // Test empty string rules
+        FunctionRewriteRules rule = FunctionRewriteRules.create("mysql", "");
+
+        // Should only have default MySQL rewrite rules
+        Assertions.assertEquals("ifnull", rule.rewriteFunction("nvl"));
+        Assertions.assertEquals("date", rule.rewriteFunction("to_date"));
+        Assertions.assertEquals("other_function", 
rule.rewriteFunction("other_function"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateCaseInsensitiveDataSource() {
+        // Test case insensitivity for datasource names
+        FunctionRewriteRules mysqlRule = FunctionRewriteRules.create("MYSQL", 
null);
+        FunctionRewriteRules clickhouseRule = 
FunctionRewriteRules.create("ClickHouse", null);
+        FunctionRewriteRules oracleRule = 
FunctionRewriteRules.create("Oracle", null);
+
+        // Should apply correct default rules regardless of case
+        Assertions.assertEquals("ifnull", mysqlRule.rewriteFunction("nvl"));
+        Assertions.assertEquals("FROM_UNIXTIME", 
clickhouseRule.rewriteFunction("from_unixtime"));
+        Assertions.assertEquals("nvl", oracleRule.rewriteFunction("ifnull"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleRewriteFunction() {
+        // Test the rewriteFunction logic with different scenarios
+
+        // Test with custom rewrite rules
+        String jsonRules = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"func1\": \"replacement1\",\n"
+                + "    \"func2\": \"replacement2\"\n"
+                + "  }\n"
+                + "}";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("unknown", 
jsonRules);
+
+        // Functions in rewrite map should be replaced
+        Assertions.assertEquals("replacement1", rule.rewriteFunction("func1"));
+        Assertions.assertEquals("replacement2", rule.rewriteFunction("func2"));
+
+        // Functions not in rewrite map should return original name
+        Assertions.assertEquals("func3", rule.rewriteFunction("func3"));
+        Assertions.assertEquals("unknown_func", 
rule.rewriteFunction("unknown_func"));
+
+        // Test with null function name
+        Assertions.assertEquals(null, rule.rewriteFunction(null));
+
+        // Test with empty function name
+        Assertions.assertEquals("", rule.rewriteFunction(""));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheck() throws DdlException {
+        // Test valid JSON rewrite rules
+        String validJson = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"func1\": \"replacement1\",\n"
+                + "    \"func2\": \"replacement2\"\n"
+                + "  }\n"
+                + "}";
+
+        // Should not throw exception
+        Assertions.assertDoesNotThrow(() -> {
+            FunctionRewriteRules.check(validJson);
+        });
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheckWithInvalidJson() {
+        // Test invalid JSON rules
+        String invalidJson = "{ invalid json }";
+
+        // Should throw DdlException
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionRewriteRules.check(invalidJson);
+        });
+
+        Assertions.assertTrue(exception.getMessage().contains("Failed to parse 
rewrite rules"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheckWithEmptyJson() throws 
DdlException {
+        // Test empty JSON
+        String emptyJson = "{}";
+
+        // Should not throw exception
+        Assertions.assertDoesNotThrow(() -> {
+            FunctionRewriteRules.check(emptyJson);
+        });
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheckWithNullJson() {
+        // Test null JSON
+        String nullJson = null;
+
+        // Should throw DdlException due to new null check
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionRewriteRules.check(nullJson);
+        });
+        Assertions.assertTrue(exception.getMessage().contains("Failed to parse 
rewrite rules"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheckWithMalformedJson() {
+        // Test various malformed JSON strings
+        String[] malformedJsons = {
+                "{ \"rewrite\": }",                    // Missing value
+                "{ \"rewrite\": { } }", // Missing object - this is actually 
valid now
+                "{ \"rewrite\" { } }", // Missing colon
+                "{ \"rewrite\": {\"func1\": \"replacement1\",} }", // Trailing 
comma
+                "{ \"rewrite\": {\"func1\" \"replacement1\"} }" // Missing 
colon
+        };
+
+        for (String malformedJson : malformedJsons) {
+            // Skip the second case as it's now valid
+            if (malformedJson.equals("{ \"rewrite\": { } }")) {
+                continue;
+            }
+
+            DdlException exception = 
Assertions.assertThrows(DdlException.class, () -> {
+                FunctionRewriteRules.check(malformedJson);
+            });
+
+            Assertions.assertTrue(exception.getMessage().contains("Failed to 
parse rewrite rules"));
+        }
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheckWithEmptyFunctionNames() {
+        // Test empty function names in rewrite rules should throw exception
+        String emptyKeyJson = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"\": \"replacement1\",\n"
+                + "    \"func2\": \"replacement2\"\n"
+                + "  }\n"
+                + "}";
+
+        DdlException exception1 = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionRewriteRules.check(emptyKeyJson);
+        });
+        Assertions.assertTrue(exception1.getMessage().contains("Failed to 
parse rewrite rules"));
+
+        String emptyValueJson = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"func1\": \"\",\n"
+                + "    \"func2\": \"replacement2\"\n"
+                + "  }\n"
+                + "}";
+
+        DdlException exception2 = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionRewriteRules.check(emptyValueJson);
+        });
+        Assertions.assertTrue(exception2.getMessage().contains("Failed to 
parse rewrite rules"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCheckWithNullFunctionNames() {
+        // Test null function names in rewrite rules should throw exception
+        // Note: JSON parsing will not allow null keys, but null values are 
possible
+        String nullValueJson = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"func1\": null,\n"
+                + "    \"func2\": \"replacement2\"\n"
+                + "  }\n"
+                + "}";
+
+        DdlException exception = Assertions.assertThrows(DdlException.class, 
() -> {
+            FunctionRewriteRules.check(nullValueJson);
+        });
+        Assertions.assertTrue(exception.getMessage().contains("Failed to parse 
rewrite rules"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleWithComplexCustomRules() {
+        // Test complex custom rewrite rules that override and extend default 
rules
+        String complexJson = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"nvl\": \"custom_nvl\",\n"
+                + "    \"custom_func1\": \"transformed_func1\",\n"
+                + "    \"old_function\": \"new_function\"\n"
+                + "  }\n"
+                + "}";
+
+        // Test with MySQL (has default rewrite rules)
+        FunctionRewriteRules mysqlRule = FunctionRewriteRules.create("mysql", 
complexJson);
+
+        // Custom rewrite rules should override default rules
+        Assertions.assertEquals("custom_nvl", 
mysqlRule.rewriteFunction("nvl")); // overridden
+        Assertions.assertEquals("transformed_func1", 
mysqlRule.rewriteFunction("custom_func1")); // custom
+        Assertions.assertEquals("new_function", 
mysqlRule.rewriteFunction("old_function")); // custom
+
+        // Default MySQL rewrite rules that are not overridden should still 
work
+        Assertions.assertEquals("date", mysqlRule.rewriteFunction("to_date")); 
// default
+
+        // Functions not in any rewrite map should return original name
+        Assertions.assertEquals("sum", mysqlRule.rewriteFunction("sum"));
+        Assertions.assertEquals("count", mysqlRule.rewriteFunction("count"));
+    }
+
+    @Test
+    public void testFunctionRewriteRuleCreateWithMultipleDataSources() {
+        // Test that different datasources have different default rules
+        FunctionRewriteRules mysqlRule = FunctionRewriteRules.create("mysql", 
null);
+        FunctionRewriteRules clickhouseRule = 
FunctionRewriteRules.create("clickhouse", null);
+        FunctionRewriteRules oracleRule = 
FunctionRewriteRules.create("oracle", null);
+
+        // Same function should be rewritten differently for different 
datasources
+        Assertions.assertEquals("ifnull", mysqlRule.rewriteFunction("nvl")); 
// MySQL: nvl -> ifnull
+        Assertions.assertEquals("nvl", clickhouseRule.rewriteFunction("nvl")); 
// ClickHouse: no rewrite
+        Assertions.assertEquals("nvl", oracleRule.rewriteFunction("nvl")); // 
Oracle: no rewrite
+
+        Assertions.assertEquals("ifnull", 
mysqlRule.rewriteFunction("ifnull")); // MySQL: no rewrite
+        Assertions.assertEquals("ifnull", 
clickhouseRule.rewriteFunction("ifnull")); // ClickHouse: no rewrite
+        Assertions.assertEquals("nvl", oracleRule.rewriteFunction("ifnull")); 
// Oracle: ifnull -> nvl
+
+        Assertions.assertEquals("FROM_UNIXTIME",
+                clickhouseRule.rewriteFunction("from_unixtime")); // 
ClickHouse specific
+        Assertions.assertEquals("from_unixtime", 
mysqlRule.rewriteFunction("from_unixtime")); // No rewrite in MySQL
+        Assertions.assertEquals("from_unixtime", 
oracleRule.rewriteFunction("from_unixtime")); // No rewrite in Oracle
+    }
+
+    @Test
+    public void testFunctionRewriteRuleRewriteFunctionEdgeCases() {
+        // Test edge cases for rewriteFunction method
+        String jsonRules = "{\n"
+                + "  \"rewrite\": {\n"
+                + "    \"normal_func\": \"replaced_func\",\n"
+                + "    \"UPPER_CASE\": \"lower_case\",\n"
+                + "    \"Mixed_Case\": \"another_case\"\n"
+                + "  }\n"
+                + "}";
+
+        FunctionRewriteRules rule = FunctionRewriteRules.create("unknown", 
jsonRules);
+
+        // Test exact matches
+        Assertions.assertEquals("replaced_func", 
rule.rewriteFunction("normal_func"));
+        Assertions.assertEquals("lower_case", 
rule.rewriteFunction("UPPER_CASE"));
+        Assertions.assertEquals("another_case", 
rule.rewriteFunction("Mixed_Case"));
+
+        // Test case sensitivity - should not match different cases
+        Assertions.assertEquals("Normal_Func", 
rule.rewriteFunction("Normal_Func")); // different case
+        Assertions.assertEquals("upper_case", 
rule.rewriteFunction("upper_case")); // different case
+        Assertions.assertEquals("mixed_case", 
rule.rewriteFunction("mixed_case")); // different case
+
+        // Test special characters
+        Assertions.assertEquals("func_with_underscore", 
rule.rewriteFunction("func_with_underscore"));
+        Assertions.assertEquals("func123", rule.rewriteFunction("func123"));
+        Assertions.assertEquals("func-with-dash", 
rule.rewriteFunction("func-with-dash"));
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lowercase/ExternalTableNameComparedLowercaseMetaCacheFalseTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lowercase/ExternalTableNameComparedLowercaseMetaCacheFalseTest.java
index e470cebbeec..4f0c6742240 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/lowercase/ExternalTableNameComparedLowercaseMetaCacheFalseTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/lowercase/ExternalTableNameComparedLowercaseMetaCacheFalseTest.java
@@ -76,7 +76,6 @@ public class 
ExternalTableNameComparedLowercaseMetaCacheFalseTest extends TestWi
         env.getCatalogMgr().dropCatalog(stmt);
     }
 
-
     @Test
     public void testGlobalVariable() {
         Assertions.assertEquals(2, GlobalVariable.lowerCaseTableNames);
diff --git 
a/regression-test/suites/external_table_p0/jdbc/test_clickhouse_jdbc_catalog.groovy
 
b/regression-test/suites/external_table_p0/jdbc/test_clickhouse_jdbc_catalog.groovy
index 3e625596d99..9d4295fc633 100644
--- 
a/regression-test/suites/external_table_p0/jdbc/test_clickhouse_jdbc_catalog.groovy
+++ 
b/regression-test/suites/external_table_p0/jdbc/test_clickhouse_jdbc_catalog.groovy
@@ -143,6 +143,75 @@ suite("test_clickhouse_jdbc_catalog", 
"p0,external,clickhouse,external_docker,ex
         order_qt_clickhouse_7_schema_tvf """ select * from query('catalog' = 
'clickhouse_7_schema', 'query' = 'select * from doris_test.type;') order by 1; 
"""
         order_qt_clickhouse_7_schema_tvf_arr """ select * from query('catalog' 
= 'clickhouse_7_schema', 'query' = 'select * from doris_test.arr;') order by 1; 
"""
 
-        sql """ drop catalog if exists clickhouse_7_schema """
+        // test function rules
+        // test push down
+        sql """ drop catalog if exists clickhouse_7_catalog """
+        // test invalid config
+        test {
+            sql """ create catalog if not exists clickhouse_7_catalog 
properties(
+                        "type"="jdbc",
+                        "user"="default",
+                        "password"="123456",
+                        "jdbc_url" = 
"jdbc:clickhouse://${externalEnvIp}:${clickhouse_port}/doris_test?databaseTerm=schema",
+                        "driver_url" = "${driver_url_7}",
+                        "driver_class" = 
"com.clickhouse.jdbc.ClickHouseDriver",
+                        "function_rules" = '{"pushdown" : {"supported" : 
[null]}}'
+            );"""
+
+            exception """Failed to parse push down rules: {"pushdown" : 
{"supported" : [null]}}"""
+        }
+
+        sql """ create catalog if not exists clickhouse_7_catalog properties(
+                    "type"="jdbc",
+                    "user"="default",
+                    "password"="123456",
+                    "jdbc_url" = 
"jdbc:clickhouse://${externalEnvIp}:${clickhouse_port}/doris_test?databaseTerm=schema",
+                    "driver_url" = "${driver_url_7}",
+                    "driver_class" = "com.clickhouse.jdbc.ClickHouseDriver",
+                    "function_rules" = '{"pushdown" : {"supported": ["abs"]}}'
+        );"""
+        sql "use clickhouse_7_catalog.doris_test"
+        explain {
+            sql("select k4 from type where abs(k4) > 0 and unix_timestamp(k4) 
> 0")
+            contains """SELECT "k4" FROM "doris_test"."type" WHERE ((abs("k4") 
> 0)) AND ((toUnixTimestamp("k4") > 0))"""
+            contains """PREDICATES: ((abs(CAST(k4[#3] AS double)) > 0) AND 
(unix_timestamp(k4[#3]) > 0))"""
+        }
+        sql """alter catalog clickhouse_7_catalog set 
properties("function_rules" = '');"""
+        explain {
+            sql("select k4 from type where abs(k4) > 0 and unix_timestamp(k4) 
> 0")
+            contains """QUERY: SELECT "k4" FROM "doris_test"."type" WHERE 
((toUnixTimestamp("k4") > 0))"""
+            contains """PREDICATES: ((abs(CAST(k4[#3] AS double)) > 0) AND 
(unix_timestamp(k4[#3]) > 0))"""
+        }
+
+        sql """alter catalog clickhouse_7_catalog set 
properties("function_rules" = '{"pushdown" : {"supported": ["abs"]}}')"""       
  
+        explain {
+            sql("select k4 from type where abs(k4) > 0 and unix_timestamp(k4) 
> 0")
+            contains """SELECT "k4" FROM "doris_test"."type" WHERE ((abs("k4") 
> 0)) AND ((toUnixTimestamp("k4") > 0))"""
+            contains """PREDICATES: ((abs(CAST(k4[#3] AS double)) > 0) AND 
(unix_timestamp(k4[#3]) > 0))"""
+        }
+
+        // test rewrite
+        sql """alter catalog clickhouse_7_catalog set 
properties("function_rules" = '{"pushdown" : {"supported": ["abs"]}, "rewrite" 
: {"unix_timestamp" : "rewrite_func"}}')"""
+        explain {
+            sql("select k4 from type where abs(k4) > 0 and unix_timestamp(k4) 
> 0")
+            contains """QUERY: SELECT "k4" FROM "doris_test"."type" WHERE 
((abs("k4") > 0)) AND ((rewrite_func("k4") > 0))"""
+            contains """((abs(CAST(k4[#3] AS double)) > 0) AND 
(unix_timestamp(k4[#3]) > 0))"""
+        }
+
+        // reset function rules
+        sql """alter catalog clickhouse_7_catalog set 
properties("function_rules" = '');"""
+        explain {
+            sql("select k4 from type where abs(k4) > 0 and unix_timestamp(k4) 
> 0")
+            contains """QUERY: SELECT "k4" FROM "doris_test"."type" WHERE 
((toUnixTimestamp("k4") > 0))"""
+            contains """PREDICATES: ((abs(CAST(k4[#3] AS double)) > 0) AND 
(unix_timestamp(k4[#3]) > 0))"""
+        }
+
+        // test invalid config
+        test {
+            sql """alter catalog clickhouse_7_catalog set 
properties("function_rules" = 'invalid_json')"""
+            exception """Failed to parse push down rules: invalid_json"""
+        }
+
+        // sql """ drop catalog if exists clickhouse_7_schema """
     }
 }
diff --git 
a/regression-test/suites/external_table_p0/jdbc/test_jdbc_query_mysql.groovy 
b/regression-test/suites/external_table_p0/jdbc/test_jdbc_query_mysql.groovy
index 4a32e2e206a..16c99826311 100644
--- a/regression-test/suites/external_table_p0/jdbc/test_jdbc_query_mysql.groovy
+++ b/regression-test/suites/external_table_p0/jdbc/test_jdbc_query_mysql.groovy
@@ -941,6 +941,121 @@ suite("test_jdbc_query_mysql", 
"p0,external,mysql,external_docker,external_docke
         order_qt_sql111 """ SELECT rank() OVER () FROM (SELECT k8 FROM 
$jdbcMysql57Table1 LIMIT 10) as t LIMIT 3 """
         order_qt_sql112 """ SELECT k7, count(DISTINCT k8) FROM 
$jdbcMysql57Table1 WHERE k8 > 110 GROUP BY GROUPING SETS ((), (k7)) """
 
+        // test function rules
+        sql  """ drop table if exists jdbc_table_function_rule """
+        test {
+            sql  """ 
+                    CREATE EXTERNAL TABLE `jdbc_table_function_rule` (
+                       `products_id` int(11) NOT NULL,
+                       `orders_id` int(11) NOT NULL,
+                       `sales_add_time` datetime NOT NULL,
+                       `sales_update_time` datetime NOT NULL,
+                       `finance_admin` int(11) NOT NULL
+                    ) ENGINE=JDBC
+                    COMMENT "JDBC Mysql 外部表"
+                    PROPERTIES (
+                    "resource" = "$jdbcResourceMysql57",
+                    "table" = "ex_tb4",
+                    "table_type"="mysql",
+                    "function_rules" = '{"pushdown" : {"supported" : [null]}}'
+                    ); 
+            """
+
+            exception """Failed to parse push down rules: {"pushdown" : 
{"supported" : [null]}}"""
+        }
+
+        sql  """ 
+                CREATE EXTERNAL TABLE `jdbc_table_function_rule` (
+                   `products_id` int(11) NOT NULL,
+                   `orders_id` int(11) NOT NULL,
+                   `sales_add_time` datetime NOT NULL,
+                   `sales_update_time` datetime NOT NULL,
+                   `finance_admin` int(11) NOT NULL
+                ) ENGINE=JDBC
+                COMMENT "JDBC Mysql 外部表"
+                PROPERTIES (
+                "resource" = "$jdbcResourceMysql57",
+                "table" = "ex_tb4",
+                "table_type"="mysql",
+                "function_rules" = '{"pushdown" : {"supported" : 
["date_trunc"]}}'
+                ); 
+        """
+        explain {
+            sql """select products_id from jdbc_table_function_rule where 
abs(products_id) > 0 and date_trunc(`sales_add_time`, "month") = "2013-10-01 
00:00:00";"""
+            contains """QUERY: SELECT `products_id`, `sales_add_time` FROM 
`ex_tb4` WHERE (date_trunc(`sales_add_time`, 'month') = '2013-10-01 
00:00:00')"""
+            contains """PREDICATES: ((abs(products_id[#0]) > 0) AND 
(date_trunc(sales_add_time[#2], 'month') = '2013-10-01 00:00:00'))"""
+        }
+
+        sql """drop table jdbc_table_function_rule"""
+        sql  """ 
+                CREATE EXTERNAL TABLE `jdbc_table_function_rule` (
+                   `products_id` int(11) NOT NULL,
+                   `orders_id` int(11) NOT NULL,
+                   `sales_add_time` datetime NOT NULL,
+                   `sales_update_time` datetime NOT NULL,
+                   `finance_admin` int(11) NOT NULL
+                ) ENGINE=JDBC
+                COMMENT "JDBC Mysql 外部表"
+                PROPERTIES (
+                "resource" = "$jdbcResourceMysql57",
+                "table" = "ex_tb4",
+                "table_type"="mysql",
+                "function_rules" = ''
+                ); 
+        """
+        explain {
+            sql """select products_id from jdbc_table_function_rule where 
abs(products_id) > 0 and date_trunc(`sales_add_time`, "month") = "2013-10-01 
00:00:00";"""
+            contains """QUERY: SELECT `products_id`, `sales_add_time` FROM 
`ex_tb4` WHERE ((abs(`products_id`) > 0))"""
+            contains """PREDICATES: ((abs(products_id[#0]) > 0) AND 
(date_trunc(sales_add_time[#2], 'month') = '2013-10-01 00:00:00'))"""
+        }
+
+        sql """drop table jdbc_table_function_rule"""
+        sql  """ 
+                CREATE EXTERNAL TABLE `jdbc_table_function_rule` (
+                   `products_id` int(11) NOT NULL,
+                   `orders_id` int(11) NOT NULL,
+                   `sales_add_time` datetime NOT NULL,
+                   `sales_update_time` datetime NOT NULL,
+                   `finance_admin` int(11) NOT NULL
+                ) ENGINE=JDBC
+                COMMENT "JDBC Mysql 外部表"
+                PROPERTIES (
+                "resource" = "$jdbcResourceMysql57",
+                "table" = "ex_tb4",
+                "table_type"="mysql",
+                "function_rules" = '{"pushdown" : {"supported": 
["date_trunc"], "unsupported" : ["abs"]}}'
+                ); 
+        """
+        explain {
+            sql """select products_id from jdbc_table_function_rule where 
abs(products_id) > 0 and date_trunc(`sales_add_time`, "month") = "2013-10-01 
00:00:00";"""
+            contains """QUERY: SELECT `products_id`, `sales_add_time` FROM 
`ex_tb4` WHERE (date_trunc(`sales_add_time`, 'month') = '2013-10-01 
00:00:00')"""
+            contains """PREDICATES: ((abs(products_id[#0]) > 0) AND 
(date_trunc(sales_add_time[#2], 'month') = '2013-10-01 00:00:00'))"""
+        }
+
+        // test rewrite
+        sql """drop table jdbc_table_function_rule"""
+        sql  """ 
+                CREATE EXTERNAL TABLE `jdbc_table_function_rule` (
+                   `products_id` int(11) NOT NULL,
+                   `orders_id` int(11) NOT NULL,
+                   `sales_add_time` datetime NOT NULL,
+                   `sales_update_time` datetime NOT NULL,
+                   `finance_admin` int(11) NOT NULL
+                ) ENGINE=JDBC
+                COMMENT "JDBC Mysql 外部表"
+                PROPERTIES (
+                "resource" = "$jdbcResourceMysql57",
+                "table" = "ex_tb4",
+                "table_type"="mysql",
+                "function_rules" = '{"pushdown" : {"supported": ["to_date"], 
"unsupported" : ["abs"]}, "rewrite" : {"to_date" : "date2"}}'
+                ); 
+        """
+        explain {
+            sql """select products_id from jdbc_table_function_rule where 
to_date(sales_add_time) = "2013-10-01" and  abs(products_id) > 0 and 
date_trunc(`sales_add_time`, "month") = "2013-10-01 00:00:00";"""
+            contains """QUERY: SELECT `products_id`, `sales_add_time` FROM 
`ex_tb4` WHERE (date2(`sales_add_time`) = '2013-10-01')"""
+            contains """PREDICATES: (((to_date(sales_add_time[#2]) = 
'2013-10-01') AND (abs(products_id[#0]) > 0)) AND 
(date_trunc(sales_add_time[#2], 'month') = '2013-10-01 00:00:00'))"""
+        }
+
         // TODO: check this, maybe caused by datasource in JDBC
         // test alter resource
         sql """alter resource $jdbcResourceMysql57 properties("password" = 
"1234567")"""
@@ -950,25 +1065,6 @@ suite("test_jdbc_query_mysql", 
"p0,external,mysql,external_docker,external_docke
         }
         sql """alter resource $jdbcResourceMysql57 properties("password" = 
"123456")"""
 
-//         // test for type check
-//         sql  """ drop table if exists ${exMysqlTypeTable} """
-//         sql  """
-//                CREATE EXTERNAL TABLE ${exMysqlTypeTable} (
-//                `id` bigint NOT NULL,
-//                `count_value` varchar(100) NULL
-//                ) ENGINE=JDBC
-//                COMMENT "JDBC Mysql 外部表"
-//                PROPERTIES (
-//                 "resource" = "$jdbcResourceMysql57",
-//                 "table" = "ex_tb2",
-//                 "table_type"="mysql"
-//                );
-//         """
-//
-//         test {
-//             sql """select * from ${exMysqlTypeTable} order by id"""
-//             exception "Fail to convert jdbc type of java.lang.Integer to 
doris type BIGINT on column: id"
-//         }
 
     }
 }
diff --git 
a/regression-test/suites/external_table_p0/jdbc/test_mysql_jdbc_catalog.groovy 
b/regression-test/suites/external_table_p0/jdbc/test_mysql_jdbc_catalog.groovy
index 46f2bb371e9..2e06703e115 100644
--- 
a/regression-test/suites/external_table_p0/jdbc/test_mysql_jdbc_catalog.groovy
+++ 
b/regression-test/suites/external_table_p0/jdbc/test_mysql_jdbc_catalog.groovy
@@ -72,6 +72,7 @@ suite("test_mysql_jdbc_catalog", 
"p0,external,mysql,external_docker,external_doc
         String dt_null = "dt_null";
         String test_zd = "test_zd"
 
+        sql """switch internal"""
         try_sql("DROP USER ${user}")
         sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
 
@@ -96,7 +97,7 @@ suite("test_mysql_jdbc_catalog", 
"p0,external,mysql,external_docker,external_doc
             "driver_class" = "${driver_class}"
         );"""
 
-        sql """use ${internal_db_name}"""
+        sql """use internal.${internal_db_name}"""
         sql  """ drop table if exists ${internal_db_name}.${inDorisTable} """
         sql  """
               CREATE TABLE ${internal_db_name}.${inDorisTable} (
@@ -653,6 +654,79 @@ suite("test_mysql_jdbc_catalog", 
"p0,external,mysql,external_docker,external_doc
         // so need to test both.
         sql """drop catalog if exists mysql_conjuncts;"""
         sql """set enable_nereids_planner=true"""
+
+        
+        // test function rules
+        // test push down
+        sql """ drop catalog if exists mysql_function_rules"""
+        // test invalid config
+        test {
+            sql """create catalog if not exists mysql_function_rules 
properties(
+                "type"="jdbc",
+                "user"="root",
+                "password"="123456",
+                "jdbc_url" = 
"jdbc:mysql://${externalEnvIp}:${mysql_port}/doris_test?useSSL=false&zeroDateTimeBehavior=convertToNull",
+                "driver_url" = "${driver_url}",
+                "driver_class" = "${driver_class}",
+                "metadata_refresh_interval_sec" = "5",
+                "function_rules" = '{"pushdown" : {"supported" : [null]}}'
+            );"""
+
+            exception """Failed to parse push down rules: {"pushdown" : 
{"supported" : [null]}}"""
+        }
+
+        sql """create catalog if not exists mysql_function_rules properties(
+            "type"="jdbc",
+            "user"="root",
+            "password"="123456",
+            "jdbc_url" = 
"jdbc:mysql://${externalEnvIp}:${mysql_port}/doris_test?useSSL=false&zeroDateTimeBehavior=convertToNull",
+            "driver_url" = "${driver_url}",
+            "driver_class" = "${driver_class}",
+            "metadata_refresh_interval_sec" = "5",
+            "function_rules" = '{"pushdown" : {"supported" : ["date_trunc"]}}'
+        );"""
+
+        sql "use mysql_function_rules.doris_test"
+        explain {
+            sql """select tinyint_u from all_types where abs(tinyint_u) > 0 
and date_trunc(`datetime`, "month") = "2013-10-01 00:00:00";"""
+            contains """QUERY: SELECT `tinyint_u`, `datetime` FROM 
`doris_test`.`all_types` WHERE (date_trunc(`datetime`, 'month') = '2013-10-01 
00:00:00')"""
+            contains """PREDICATES: ((abs(tinyint_u[#0]) > 0) AND 
(date_trunc(datetime[#17], 'month') = '2013-10-01 00:00:00'))"""
+        }
+        sql """alter catalog mysql_function_rules set 
properties("function_rules" = '');"""
+        explain {
+            sql """select tinyint_u from all_types where abs(tinyint_u) > 0 
and date_trunc(`datetime`, "month") = "2013-10-01 00:00:00";"""
+            contains """QUERY: SELECT `tinyint_u`, `datetime` FROM 
`doris_test`.`all_types` WHERE ((abs(`tinyint_u`) > 0))"""
+            contains """PREDICATES: ((abs(tinyint_u[#0]) > 0) AND 
(date_trunc(datetime[#17], 'month') = '2013-10-01 00:00:00'))"""
+        }
+
+        sql """alter catalog mysql_function_rules set 
properties("function_rules" = '{"pushdown" : {"supported": ["date_trunc"], 
"unsupported" : ["abs"]}}')"""         
+        explain {
+            sql """select tinyint_u from all_types where abs(tinyint_u) > 0 
and date_trunc(`datetime`, "month") = "2013-10-01 00:00:00";"""
+            contains """QUERY: SELECT `tinyint_u`, `datetime` FROM 
`doris_test`.`all_types` WHERE (date_trunc(`datetime`, 'month') = '2013-10-01 
00:00:00')"""
+            contains """PREDICATES: ((abs(tinyint_u[#0]) > 0) AND 
(date_trunc(datetime[#17], 'month') = '2013-10-01 00:00:00'))"""
+        }
+
+        // test rewrite
+        sql """alter catalog mysql_function_rules set 
properties("function_rules" = '{"pushdown" : {"supported": ["to_date"], 
"unsupported" : ["abs"]}, "rewrite" : {"to_date" : "date2"}}');"""
+        explain {
+            sql """select tinyint_u from all_types where to_date(`datetime`) = 
"2013-10-01" and abs(tinyint_u) > 0 and date_trunc(`datetime`, "month") = 
"2013-10-01 00:00:00";"""
+            contains """QUERY: SELECT `tinyint_u`, `datetime` FROM 
`doris_test`.`all_types` WHERE (date2(`datetime`) = '2013-10-01')"""
+            contains """PREDICATES: (((to_date(datetime[#17]) = '2013-10-01') 
AND (abs(tinyint_u[#0]) > 0)) AND (date_trunc(datetime[#17], 'month') = 
'2013-10-01 00:00:00'))"""
+        }
+
+        // reset function rules
+        sql """alter catalog mysql_function_rules set 
properties("function_rules" = '');"""
+        explain {
+            sql """select tinyint_u from all_types where to_date(`datetime`) = 
"2013-10-01" and abs(tinyint_u) > 0 and date_trunc(`datetime`, "month") = 
"2013-10-01 00:00:00";"""
+            contains """QUERY: SELECT `tinyint_u`, `datetime` FROM 
`doris_test`.`all_types` WHERE (date(`datetime`) = '2013-10-01') AND 
((abs(`tinyint_u`) > 0))"""
+            contains """PREDICATES: (((to_date(datetime[#17]) = '2013-10-01') 
AND (abs(tinyint_u[#0]) > 0)) AND (date_trunc(datetime[#17], 'month') = 
'2013-10-01 00:00:00'))"""
+        }
+
+        // test invalid config
+        test {
+            sql """alter catalog mysql_function_rules set 
properties("function_rules" = 'invalid_json')"""
+            exception """Failed to parse push down rules: invalid_json"""
+        }
     }
 }
 
diff --git 
a/regression-test/suites/external_table_p0/jdbc/test_oracle_jdbc_catalog.groovy 
b/regression-test/suites/external_table_p0/jdbc/test_oracle_jdbc_catalog.groovy
index 4dd2607d484..818de72bca7 100644
--- 
a/regression-test/suites/external_table_p0/jdbc/test_oracle_jdbc_catalog.groovy
+++ 
b/regression-test/suites/external_table_p0/jdbc/test_oracle_jdbc_catalog.groovy
@@ -387,7 +387,77 @@ suite("test_oracle_jdbc_catalog", 
"p0,external,oracle,external_docker,external_d
         order_qt_null_operator9 """ SELECT * FROM STUDENT WHERE (id IS NOT 
NULL AND NULL); """
         order_qt_null_operator10 """ SELECT * FROM STUDENT WHERE (name IS NULL 
OR age IS NOT NULL); """
 
-        sql """ drop catalog if exists oracle_null_operator; """
+        // test function rules
+        // test push down
+        sql """ drop catalog if exists oracle_function_rules"""
+        // test invalid config
+        test {
+            sql """create catalog if not exists oracle_function_rules 
properties(
+                "type"="jdbc",
+                "user"="doris_test",
+                "password"="123456",
+                "jdbc_url" = 
"jdbc:oracle:thin:@${externalEnvIp}:${oracle_port}:${SID}",
+                "driver_url" = "${driver_url}",
+                "driver_class" = "oracle.jdbc.driver.OracleDriver",
+                "function_rules" = '{"pushdown" : {"supported" : [null]}}'
+            );"""
+
+            exception """Failed to parse push down rules: {"pushdown" : 
{"supported" : [null]}}"""
+        }
+
+        sql """create catalog if not exists oracle_function_rules properties(
+            "type"="jdbc",
+            "user"="doris_test",
+            "password"="123456",
+            "jdbc_url" = 
"jdbc:oracle:thin:@${externalEnvIp}:${oracle_port}:${SID}",
+            "driver_url" = "${driver_url}",
+            "driver_class" = "oracle.jdbc.driver.OracleDriver",
+            "function_rules" = '{"pushdown" : {"supported" : ["abs"]}}'
+        );"""
+
+        sql "use oracle_function_rules.DORIS_TEST"
+        explain {
+            sql """select id from STUDENT where abs(id) > 0 and ifnull(id, 3) 
= 3;"""
+            contains """QUERY: SELECT "ID" FROM "DORIS_TEST"."STUDENT" WHERE 
((abs("ID") > 0)) AND ((nvl("ID", 3) = 3))"""
+            contains """PREDICATES: ((abs(ID[#0]) > 0) AND (ifnull(ID[#0], 3) 
= 3))"""
+        }
+        sql """alter catalog oracle_function_rules set 
properties("function_rules" = '');"""
+        explain {
+            sql """select id from STUDENT where abs(id) > 0 and ifnull(id, 3) 
= 3;"""
+            contains """QUERY: SELECT "ID" FROM "DORIS_TEST"."STUDENT" WHERE 
((nvl("ID", 3) = 3))"""
+            contains """PREDICATES: ((abs(ID[#0]) > 0) AND (ifnull(ID[#0], 3) 
= 3))"""
+        }
+
+        sql """alter catalog oracle_function_rules set 
properties("function_rules" = '{"pushdown" : {"supported": ["abs"], 
"unsupported" : []}}')"""
+        explain {
+            sql """select id from STUDENT where abs(id) > 0 and ifnull(id, 3) 
= 3;"""
+            contains """QUERY: SELECT "ID" FROM "DORIS_TEST"."STUDENT" WHERE 
((abs("ID") > 0)) AND ((nvl("ID", 3) = 3))"""
+            contains """PREDICATES: ((abs(ID[#0]) > 0) AND (ifnull(ID[#0], 3) 
= 3))"""
+        }
+
+        // test rewrite
+        sql """alter catalog oracle_function_rules set 
properties("function_rules" = '{"pushdown" : {"supported": ["abs"]}, "rewrite" 
: {"abs" : "abs2"}}');"""
+        explain {
+            sql """select id from STUDENT where abs(id) > 0 and ifnull(id, 3) 
= 3;"""
+            contains """QUERY: SELECT "ID" FROM "DORIS_TEST"."STUDENT" WHERE 
((abs2("ID") > 0)) AND ((nvl("ID", 3) = 3))"""
+            contains """PREDICATES: ((abs(ID[#0]) > 0) AND (ifnull(ID[#0], 3) 
= 3))"""
+        }
+
+        // reset function rules
+        sql """alter catalog oracle_function_rules set 
properties("function_rules" = '');"""
+        explain {
+            sql """select id from STUDENT where abs(id) > 0 and ifnull(id, 3) 
= 3;"""
+            contains """QUERY: SELECT "ID" FROM "DORIS_TEST"."STUDENT" WHERE 
((nvl("ID", 3) = 3))"""
+            contains """PREDICATES: ((abs(ID[#0]) > 0) AND (ifnull(ID[#0], 3) 
= 3))"""
+        }
+
+        // test invalid config
+        test {
+            sql """alter catalog oracle_function_rules set 
properties("function_rules" = 'invalid_json')"""
+            exception """Failed to parse push down rules: invalid_json"""
+        }
+
+        // sql """ drop catalog if exists oracle_null_operator; """
 
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@doris.apache.org
For additional commands, e-mail: commits-h...@doris.apache.org

Reply via email to