Skip to content

Commit 7459e0d

Browse files
Vladsz83alex-plekhanov
authored andcommitted
IGNITE-24358 SQL Calcite: Restrict system functions overriding - Fixes #11851.
Signed-off-by: Aleksey Plekhanov <[email protected]>
1 parent 2d57d27 commit 7459e0d

File tree

3 files changed

+138
-3
lines changed

3 files changed

+138
-3
lines changed

docs/_docs/SQL/custom-sql-func.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
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.
2020

21-
A custom SQL function is just a public static method marked by the `@QuerySqlFunction` annotation.
21+
A custom SQL function is just a `public` or `public static` method marked by the `@QuerySqlFunction` annotation.
2222

2323
////
2424
TODO looks like it's unsupported in C#
@@ -48,7 +48,7 @@ include::{javaFile}[tags=sql-function-query, indent=0]
4848

4949

5050
Custom SQL function can be a table function. Result of table function is treated as a row set (a table) and can be used
51-
by other SQL operators. Custom SQL function is also a `public` method marked by annotation `@QuerySqlTableFunction`.
51+
by other SQL operators. Custom SQL function is also a `public` or `public static` method marked by annotation `@QuerySqlTableFunction`.
5252
Table function must return an `Iterable` as a row set. Each row can be represented by an `Object[]` or by a `Collection`.
5353
Row length must match the defined number of column types. Row value types must match the defined column types or be able
5454
assigned to them.
@@ -61,6 +61,8 @@ include::{javaFile}[tags=sql-table-function-example, indent=0]
6161
----
6262
include::{javaFile}[tags=sql-table-function-config-query, indent=0]
6363
----
64+
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'.
65+
6466
NOTE: The table functions also are available currently only with link:SQL/sql-calcite[Calcite, window=_blank].
6567

6668
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.

modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/schema/SchemaHolderImpl.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
import org.apache.calcite.rel.RelCollations;
2828
import org.apache.calcite.rel.RelFieldCollation;
2929
import org.apache.calcite.schema.SchemaPlus;
30+
import org.apache.calcite.sql.SqlIdentifier;
31+
import org.apache.calcite.sql.SqlOperator;
32+
import org.apache.calcite.sql.SqlSyntax;
33+
import org.apache.calcite.sql.parser.SqlParserPos;
34+
import org.apache.calcite.sql.validate.SqlNameMatchers;
3035
import org.apache.calcite.tools.FrameworkConfig;
3136
import org.apache.calcite.tools.Frameworks;
3237
import org.apache.calcite.util.ImmutableBitSet;
@@ -51,6 +56,7 @@
5156
import org.apache.ignite.internal.processors.query.schema.SchemaChangeListener;
5257
import org.apache.ignite.internal.processors.query.schema.management.IndexDescriptor;
5358
import org.apache.ignite.internal.processors.subscription.GridInternalSubscriptionProcessor;
59+
import org.apache.ignite.internal.util.typedef.F;
5460
import org.apache.ignite.internal.util.typedef.internal.S;
5561
import org.apache.ignite.lang.IgnitePredicate;
5662
import org.apache.ignite.spi.systemview.view.SystemView;
@@ -356,6 +362,9 @@ private static Object affinityIdentity(CacheConfiguration<?, ?> ccfg) {
356362

357363
/** {@inheritDoc} */
358364
@Override public void onFunctionCreated(String schemaName, String name, boolean deterministic, Method method) {
365+
if (!checkNewUserDefinedFunction(schemaName, name))
366+
return;
367+
359368
IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);
360369

361370
schema.addFunction(name.toUpperCase(), IgniteScalarFunction.create(method));
@@ -371,13 +380,40 @@ private static Object affinityIdentity(CacheConfiguration<?, ?> ccfg) {
371380
Class<?>[] colTypes,
372381
String[] colNames
373382
) {
383+
if (!checkNewUserDefinedFunction(schemaName, name))
384+
return;
385+
374386
IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);
375387

376388
schema.addFunction(name.toUpperCase(), IgniteTableFunction.create(method, colTypes, colNames));
377389

378390
rebuild();
379391
}
380392

393+
/** */
394+
private boolean checkNewUserDefinedFunction(String schName, String funName) {
395+
if (F.eq(schName, QueryUtils.DFLT_SCHEMA)) {
396+
List<SqlOperator> operators = new ArrayList<>();
397+
398+
frameworkCfg.getOperatorTable().lookupOperatorOverloads(
399+
new SqlIdentifier(funName, SqlParserPos.ZERO),
400+
null,
401+
SqlSyntax.FUNCTION,
402+
operators,
403+
SqlNameMatchers.withCaseSensitive(false)
404+
);
405+
406+
if (!operators.isEmpty()) {
407+
log.error("Unable to add user-defined SQL function '" + funName + "'. Default schema '"
408+
+ QueryUtils.DFLT_SCHEMA + "' already has a standard function with the same name.");
409+
410+
return false;
411+
}
412+
}
413+
414+
return true;
415+
}
416+
381417
/** {@inheritDoc} */
382418
@Override public void onSystemViewCreated(String schemaName, SystemView<?> sysView) {
383419
IgniteSchema schema = igniteSchemas.computeIfAbsent(schemaName, IgniteSchema::new);

modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/UserDefinedFunctionsIntegrationTest.java

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717

1818
package org.apache.ignite.internal.processors.query.calcite.integration;
1919

20+
import java.sql.Timestamp;
2021
import java.util.Arrays;
2122
import java.util.Collection;
2223
import java.util.List;
24+
import org.apache.calcite.schema.SchemaPlus;
2325
import org.apache.calcite.sql.validate.SqlValidatorException;
2426
import org.apache.ignite.IgniteCache;
2527
import org.apache.ignite.IgniteCheckedException;
@@ -32,6 +34,7 @@
3234
import org.apache.ignite.configuration.CacheConfiguration;
3335
import org.apache.ignite.configuration.IgniteConfiguration;
3436
import org.apache.ignite.internal.processors.query.IgniteSQLException;
37+
import org.apache.ignite.internal.processors.query.QueryUtils;
3538
import org.apache.ignite.internal.util.typedef.F;
3639
import org.apache.ignite.testframework.GridTestUtils;
3740
import org.apache.ignite.testframework.ListeningTestLogger;
@@ -61,6 +64,67 @@ public class UserDefinedFunctionsIntegrationTest extends AbstractBasicIntegratio
6164
return cfg;
6265
}
6366

67+
/** */
68+
@Test
69+
public void testSystemFunctionOverriding() throws Exception {
70+
// To a custom schema.
71+
client.getOrCreateCache(new CacheConfiguration<Integer, Employer>("TEST_CACHE_OWN")
72+
.setSqlSchema("OWN_SCHEMA")
73+
.setSqlFunctionClasses(OverrideSystemFunctionLibrary.class)
74+
.setQueryEntities(F.asList(new QueryEntity(Integer.class, Employer.class).setTableName("emp_own")))
75+
);
76+
77+
// Make sure that the new functions didn't affect schema 'PUBLIC'.
78+
assertQuery("SELECT UPPER(?)").withParams("abc").returns("ABC").check();
79+
assertQuery("select UNIX_SECONDS(TIMESTAMP '2021-01-01 00:00:00')").returns(1609459200L).check();
80+
assertQuery("select * from table(SYSTEM_RANGE(1, 2))").returns(1L).returns(2L).check();
81+
assertQuery("select TYPEOF(?)").withParams(1L).returns("BIGINT").check();
82+
assertQuery("select ? + ?").withParams(1, 2).returns(3).check();
83+
assertThrows("select PLUS(?, ?)", SqlValidatorException.class, "No match found for function signature", 1, 2);
84+
85+
// Ensure that new functions are successfully created in a custom schema.
86+
assertQuery("SELECT \"OWN_SCHEMA\".UPPER(?)").withParams("abc").returns(3).check();
87+
assertQuery("select \"OWN_SCHEMA\".UNIX_SECONDS(TIMESTAMP '2021-01-01 00:00:00')").returns(1).check();
88+
assertQuery("select * from table(\"OWN_SCHEMA\".SYSTEM_RANGE(1, 2))").returns(100L).check();
89+
assertQuery("select \"OWN_SCHEMA\".TYPEOF('ABC')").returns(1).check();
90+
assertQuery("select \"OWN_SCHEMA\".PLUS(?, ?)").withParams(1, 2).returns(100).check();
91+
92+
LogListener logChecker0 = LogListener.matches("Unable to add user-defined SQL function 'upper'")
93+
.andMatches("Unable to add user-defined SQL function 'unix_seconds'")
94+
.andMatches("Unable to add user-defined SQL function 'system_range'")
95+
.andMatches("Unable to add user-defined SQL function 'typeof'")
96+
.andMatches("Unable to add user-defined SQL function 'plus'").times(0)
97+
.build();
98+
99+
listeningLog.registerListener(logChecker0);
100+
101+
// Try to add the functions into the default schema.
102+
client.getOrCreateCache(new CacheConfiguration<Integer, Employer>("TEST_CACHE_PUB")
103+
.setSqlFunctionClasses(OverrideSystemFunctionLibrary.class)
104+
.setSqlSchema(QueryUtils.DFLT_SCHEMA)
105+
.setQueryEntities(F.asList(new QueryEntity(Integer.class, Employer.class).setTableName("emp_pub"))));
106+
107+
assertTrue(logChecker0.check(getTestTimeout()));
108+
109+
// Make sure that the standard functions work once again.
110+
assertQuery("SELECT UPPER(?)").withParams("abc").returns("ABC").check();
111+
assertQuery("select UNIX_SECONDS(TIMESTAMP '2021-01-01 00:00:00')").returns(1609459200L).check();
112+
assertQuery("select * from table(SYSTEM_RANGE(1, 2))").returns(1L).returns(2L).check();
113+
assertQuery("select TYPEOF(?)").withParams(1L).returns("BIGINT").check();
114+
115+
// Make sure that operator '+' works and new function 'PLUS' also registered in the default schema.
116+
assertQuery("select ? + ?").withParams(1, 2).returns(3).check();
117+
assertQuery("select PLUS(?, ?)").withParams(1, 2).returns(100);
118+
119+
SchemaPlus dfltSchema = queryProcessor(client).schemaHolder().schema(QueryUtils.DFLT_SCHEMA);
120+
121+
assertEquals(0, dfltSchema.getFunctions("UPPER").size());
122+
assertEquals(0, dfltSchema.getFunctions("UNIX_SECONDS").size());
123+
assertEquals(0, dfltSchema.getFunctions("SYSTEM_RANGE").size());
124+
assertEquals(0, dfltSchema.getFunctions("TYPEOF").size());
125+
assertEquals(1, dfltSchema.getFunctions("PLUS").size());
126+
}
127+
64128
/** */
65129
@Test
66130
public void testFunctions() throws Exception {
@@ -260,7 +324,7 @@ public void testIncorrectTableFunctions() throws Exception {
260324
assertThrows("SELECT * FROM wrongRowType(1)", IgniteSQLException.class,
261325
"row type is neither Collection or Object[]");
262326

263-
logChecker0.check(getTestTimeout());
327+
assertTrue(logChecker0.check(getTestTimeout()));
264328

265329
String errTxt = "No match found for function signature";
266330

@@ -556,6 +620,39 @@ public static String echo(String s) {
556620
}
557621
}
558622

623+
/** */
624+
public static class OverrideSystemFunctionLibrary {
625+
/** Overwrites standard 'UPPER(VARCHAR)'. */
626+
@QuerySqlFunction
627+
public static int upper(String s) {
628+
return F.isEmpty(s) ? 0 : s.length();
629+
}
630+
631+
/** Overwrites standard 'UNIX_SECONDS(Timestamp)'. */
632+
@QuerySqlFunction
633+
public static int unix_seconds(Timestamp ts) {
634+
return 1;
635+
}
636+
637+
/** Overwrites Ignite's 'SYSTEM_RANGE(...)'. */
638+
@QuerySqlTableFunction(columnTypes = {long.class}, columnNames = {"RESULT"})
639+
public static Collection<Object> system_range(long x, long y) {
640+
return F.asList(F.asList(100L));
641+
}
642+
643+
/** Overwrites Ignite's 'TYPEOF(Object)'. */
644+
@QuerySqlFunction
645+
public static int typeof(Object o) {
646+
return 1;
647+
}
648+
649+
/** Same name as of operator '+' which is not a function. */
650+
@QuerySqlFunction
651+
public static int plus(int x, int y) {
652+
return 100;
653+
}
654+
}
655+
559656
/** */
560657
public static class InnerSqlFunctionsLibrary {
561658
/** */

0 commit comments

Comments
 (0)