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

alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 7459e0d4923 IGNITE-24358 SQL Calcite: Restrict system functions 
overriding - Fixes #11851.
7459e0d4923 is described below

commit 7459e0d492360523446572b2c4698f108da8ce6f
Author: Vladimir Steshin <[email protected]>
AuthorDate: Fri Feb 7 23:39:05 2025 +0300

    IGNITE-24358 SQL Calcite: Restrict system functions overriding - Fixes 
#11851.
    
    Signed-off-by: Aleksey Plekhanov <[email protected]>
---
 docs/_docs/SQL/custom-sql-func.adoc                |  6 +-
 .../query/calcite/schema/SchemaHolderImpl.java     | 36 ++++++++
 .../UserDefinedFunctionsIntegrationTest.java       | 99 +++++++++++++++++++++-
 3 files changed, 138 insertions(+), 3 deletions(-)

diff --git a/docs/_docs/SQL/custom-sql-func.adoc 
b/docs/_docs/SQL/custom-sql-func.adoc
index 7c9540c87ca..36d28947728 100644
--- a/docs/_docs/SQL/custom-sql-func.adoc
+++ b/docs/_docs/SQL/custom-sql-func.adoc
@@ -18,7 +18,7 @@
 
 The SQL Engine can extend the SQL functions' set, defined by the ANSI-99 
specification, via the addition of custom SQL functions written in Java.
 
-A custom SQL function is just a public static method marked by the 
`@QuerySqlFunction` annotation.
+A custom SQL function is just a `public` or `public static` method marked by 
the `@QuerySqlFunction` annotation.
 
 ////
 TODO looks like it's unsupported in C#
@@ -48,7 +48,7 @@ include::{javaFile}[tags=sql-function-query, indent=0]
 
 
 Custom SQL function can be a table function. Result of table function is 
treated as a row set (a table) and can be used
-by other SQL operators. Custom SQL function is also a `public` method marked 
by annotation `@QuerySqlTableFunction`.
+by other SQL operators. Custom SQL function is also a `public` or `public 
static` method marked by annotation `@QuerySqlTableFunction`.
 Table function must return an `Iterable` as a row set. Each row can be 
represented by an `Object[]` or by a `Collection`.
 Row length must match the defined number of column types. Row value types must 
match the defined column types or be able
 assigned to them.
@@ -61,6 +61,8 @@ include::{javaFile}[tags=sql-table-function-example, indent=0]
 ----
 include::{javaFile}[tags=sql-table-function-config-query, indent=0]
 ----
+NOTE: Creating custom functions in the system schema is forbidden. Creating a 
custom function in schema 'PUBLIC' is forbidden if the function name interferes 
with a standard function like 'TYPEOF' or 'SUBSTRING'.
+
 NOTE: The table functions also are available currently only with 
link:SQL/sql-calcite[Calcite, window=_blank].
 
 NOTE: Classes registered with `CacheConfiguration.setSqlFunctionClasses(...)` 
must be added to the classpath of all the nodes where the defined custom 
functions might be executed. Otherwise, you will get a `ClassNotFoundException` 
error when trying to execute the custom function.
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
index 2542228e654..d853a538be9 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java
@@ -27,6 +27,11 @@ import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlSyntax;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.validate.SqlNameMatchers;
 import org.apache.calcite.tools.FrameworkConfig;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.util.ImmutableBitSet;
@@ -51,6 +56,7 @@ import 
org.apache.ignite.internal.processors.query.calcite.util.Commons;
 import org.apache.ignite.internal.processors.query.schema.SchemaChangeListener;
 import 
org.apache.ignite.internal.processors.query.schema.management.IndexDescriptor;
 import 
org.apache.ignite.internal.processors.subscription.GridInternalSubscriptionProcessor;
+import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.S;
 import org.apache.ignite.lang.IgnitePredicate;
 import org.apache.ignite.spi.systemview.view.SystemView;
@@ -356,6 +362,9 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
 
     /** {@inheritDoc} */
     @Override public void onFunctionCreated(String schemaName, String name, 
boolean deterministic, Method method) {
+        if (!checkNewUserDefinedFunction(schemaName, name))
+            return;
+
         IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, 
IgniteSchema::new);
 
         schema.addFunction(name.toUpperCase(), 
IgniteScalarFunction.create(method));
@@ -371,6 +380,9 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
         Class<?>[] colTypes,
         String[] colNames
     ) {
+        if (!checkNewUserDefinedFunction(schemaName, name))
+            return;
+
         IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, 
IgniteSchema::new);
 
         schema.addFunction(name.toUpperCase(), 
IgniteTableFunction.create(method, colTypes, colNames));
@@ -378,6 +390,30 @@ public class SchemaHolderImpl extends AbstractService 
implements SchemaHolder, S
         rebuild();
     }
 
+    /** */
+    private boolean checkNewUserDefinedFunction(String schName, String 
funName) {
+        if (F.eq(schName, QueryUtils.DFLT_SCHEMA)) {
+            List<SqlOperator> operators = new ArrayList<>();
+
+            frameworkCfg.getOperatorTable().lookupOperatorOverloads(
+                new SqlIdentifier(funName, SqlParserPos.ZERO),
+                null,
+                SqlSyntax.FUNCTION,
+                operators,
+                SqlNameMatchers.withCaseSensitive(false)
+            );
+
+            if (!operators.isEmpty()) {
+                log.error("Unable to add user-defined SQL function '" + 
funName + "'. Default schema '"
+                    + QueryUtils.DFLT_SCHEMA + "' already has a standard 
function with the same name.");
+
+                return false;
+            }
+        }
+
+        return true;
+    }
+
     /** {@inheritDoc} */
     @Override public void onSystemViewCreated(String schemaName, SystemView<?> 
sysView) {
         IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, 
IgniteSchema::new);
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
index 422d3a8c2db..f46b0bb2f96 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java
@@ -17,9 +17,11 @@
 
 package org.apache.ignite.internal.processors.query.calcite.integration;
 
+import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.sql.validate.SqlValidatorException;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.IgniteCheckedException;
@@ -32,6 +34,7 @@ import 
org.apache.ignite.calcite.CalciteQueryEngineConfiguration;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.configuration.IgniteConfiguration;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.QueryUtils;
 import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.ListeningTestLogger;
@@ -61,6 +64,67 @@ public class UserDefinedFunctionsIntegrationTest extends 
AbstractBasicIntegratio
         return cfg;
     }
 
+    /** */
+    @Test
+    public void testSystemFunctionOverriding() throws Exception {
+        // To a custom schema.
+        client.getOrCreateCache(new CacheConfiguration<Integer, 
Employer>("TEST_CACHE_OWN")
+            .setSqlSchema("OWN_SCHEMA")
+            .setSqlFunctionClasses(OverrideSystemFunctionLibrary.class)
+            .setQueryEntities(F.asList(new QueryEntity(Integer.class, 
Employer.class).setTableName("emp_own")))
+        );
+
+        // Make sure that the new functions didn't affect schema 'PUBLIC'.
+        assertQuery("SELECT 
UPPER(?)").withParams("abc").returns("ABC").check();
+        assertQuery("select UNIX_SECONDS(TIMESTAMP '2021-01-01 
00:00:00')").returns(1609459200L).check();
+        assertQuery("select * from table(SYSTEM_RANGE(1, 
2))").returns(1L).returns(2L).check();
+        assertQuery("select 
TYPEOF(?)").withParams(1L).returns("BIGINT").check();
+        assertQuery("select ? + ?").withParams(1, 2).returns(3).check();
+        assertThrows("select PLUS(?, ?)", SqlValidatorException.class, "No 
match found for function signature", 1, 2);
+
+        // Ensure that new functions are successfully created in a custom 
schema.
+        assertQuery("SELECT 
\"OWN_SCHEMA\".UPPER(?)").withParams("abc").returns(3).check();
+        assertQuery("select \"OWN_SCHEMA\".UNIX_SECONDS(TIMESTAMP '2021-01-01 
00:00:00')").returns(1).check();
+        assertQuery("select * from table(\"OWN_SCHEMA\".SYSTEM_RANGE(1, 
2))").returns(100L).check();
+        assertQuery("select \"OWN_SCHEMA\".TYPEOF('ABC')").returns(1).check();
+        assertQuery("select \"OWN_SCHEMA\".PLUS(?, ?)").withParams(1, 
2).returns(100).check();
+
+        LogListener logChecker0 = LogListener.matches("Unable to add 
user-defined SQL function 'upper'")
+            .andMatches("Unable to add user-defined SQL function 
'unix_seconds'")
+            .andMatches("Unable to add user-defined SQL function 
'system_range'")
+            .andMatches("Unable to add user-defined SQL function 'typeof'")
+            .andMatches("Unable to add user-defined SQL function 
'plus'").times(0)
+            .build();
+
+        listeningLog.registerListener(logChecker0);
+
+        // Try to add the functions into the default schema.
+        client.getOrCreateCache(new CacheConfiguration<Integer, 
Employer>("TEST_CACHE_PUB")
+            .setSqlFunctionClasses(OverrideSystemFunctionLibrary.class)
+            .setSqlSchema(QueryUtils.DFLT_SCHEMA)
+            .setQueryEntities(F.asList(new QueryEntity(Integer.class, 
Employer.class).setTableName("emp_pub"))));
+
+        assertTrue(logChecker0.check(getTestTimeout()));
+
+        // Make sure that the standard functions work once again.
+        assertQuery("SELECT 
UPPER(?)").withParams("abc").returns("ABC").check();
+        assertQuery("select UNIX_SECONDS(TIMESTAMP '2021-01-01 
00:00:00')").returns(1609459200L).check();
+        assertQuery("select * from table(SYSTEM_RANGE(1, 
2))").returns(1L).returns(2L).check();
+        assertQuery("select 
TYPEOF(?)").withParams(1L).returns("BIGINT").check();
+
+        // Make sure that operator '+' works and new function 'PLUS' also 
registered in the default schema.
+        assertQuery("select ? + ?").withParams(1, 2).returns(3).check();
+        assertQuery("select PLUS(?, ?)").withParams(1, 2).returns(100);
+
+        SchemaPlus dfltSchema = 
queryProcessor(client).schemaHolder().schema(QueryUtils.DFLT_SCHEMA);
+
+        assertEquals(0, dfltSchema.getFunctions("UPPER").size());
+        assertEquals(0, dfltSchema.getFunctions("UNIX_SECONDS").size());
+        assertEquals(0, dfltSchema.getFunctions("SYSTEM_RANGE").size());
+        assertEquals(0, dfltSchema.getFunctions("TYPEOF").size());
+        assertEquals(1, dfltSchema.getFunctions("PLUS").size());
+    }
+
     /** */
     @Test
     public void testFunctions() throws Exception {
@@ -260,7 +324,7 @@ public class UserDefinedFunctionsIntegrationTest extends 
AbstractBasicIntegratio
         assertThrows("SELECT * FROM wrongRowType(1)", IgniteSQLException.class,
             "row type is neither Collection or Object[]");
 
-        logChecker0.check(getTestTimeout());
+        assertTrue(logChecker0.check(getTestTimeout()));
 
         String errTxt = "No match found for function signature";
 
@@ -556,6 +620,39 @@ public class UserDefinedFunctionsIntegrationTest extends 
AbstractBasicIntegratio
         }
     }
 
+    /** */
+    public static class OverrideSystemFunctionLibrary {
+        /** Overwrites standard 'UPPER(VARCHAR)'. */
+        @QuerySqlFunction
+        public static int upper(String s) {
+            return F.isEmpty(s) ? 0 : s.length();
+        }
+
+        /** Overwrites standard 'UNIX_SECONDS(Timestamp)'. */
+        @QuerySqlFunction
+        public static int unix_seconds(Timestamp ts) {
+            return 1;
+        }
+
+        /** Overwrites Ignite's 'SYSTEM_RANGE(...)'. */
+        @QuerySqlTableFunction(columnTypes = {long.class}, columnNames = 
{"RESULT"})
+        public static Collection<Object> system_range(long x, long y) {
+            return F.asList(F.asList(100L));
+        }
+
+        /** Overwrites Ignite's 'TYPEOF(Object)'. */
+        @QuerySqlFunction
+        public static int typeof(Object o) {
+            return 1;
+        }
+
+        /** Same name as of operator '+' which is not a function. */
+        @QuerySqlFunction
+        public static int plus(int x, int y) {
+            return 100;
+        }
+    }
+
     /** */
     public static class InnerSqlFunctionsLibrary {
         /** */

Reply via email to