Skip to content

Commit

Permalink
IGNITE-24358 SQL Calcite: Restrict system functions overriding - Fixes
Browse files Browse the repository at this point in the history
…#11851.

Signed-off-by: Aleksey Plekhanov <[email protected]>
  • Loading branch information
Vladsz83 authored and alex-plekhanov committed Feb 7, 2025
1 parent 2d57d27 commit 7459e0d
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 3 deletions.
6 changes: 4 additions & 2 deletions docs/_docs/SQL/custom-sql-func.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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#
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
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;
Expand All @@ -51,6 +56,7 @@
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;
Expand Down Expand Up @@ -356,6 +362,9 @@ private static Object affinityIdentity(CacheConfiguration<?, ?> ccfg) {

/** {@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));
Expand All @@ -371,13 +380,40 @@ private static Object affinityIdentity(CacheConfiguration<?, ?> ccfg) {
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));

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +34,7 @@
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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -260,7 +324,7 @@ public void testIncorrectTableFunctions() throws Exception {
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";

Expand Down Expand Up @@ -556,6 +620,39 @@ public static String echo(String s) {
}
}

/** */
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 {
/** */
Expand Down

0 comments on commit 7459e0d

Please sign in to comment.