Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.schema.Function;
import org.apache.calcite.schema.FunctionParameter;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.Table;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.ignite.IgniteException;
import org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
import org.apache.ignite.internal.processors.query.calcite.util.Commons;

/**
Expand Down Expand Up @@ -94,23 +96,13 @@ public void removeTable(String tblName) {
* @param func SQL function.
*/
public void addFunction(String name, Function func) {
for (Function existingFun : getFunctions(name)) {
List<FunctionParameter> params = func.getParameters();
List<FunctionParameter> existingParams = existingFun.getParameters();

if (params.size() != existingParams.size())
continue;

for (int i = 0; i < params.size(); ++i) {
FunctionParameter p = params.get(i);
FunctionParameter existingP = existingParams.get(i);
IgniteTypeFactory typeFactory = Commons.typeFactory();

if (!p.getType(Commons.typeFactory()).equalsSansFieldNames(existingP.getType(Commons.typeFactory())))
break;
for (Function existingFun : getFunctions(name)) {
if (sameParameters(func.getParameters(), existingFun.getParameters(), typeFactory)) {
throw new IgniteException("Unable to register function '" + name + "'. Other function with the same " +
"name and parameters is already registered in schema '" + schemaName + "'.");
}

throw new IgniteException("Unable to register function '" + name + "'. Other function with the same " +
"name and parameters is already registered in schema '" + schemaName + "'.");
}

funcMap.put(name, func);
Expand Down Expand Up @@ -145,4 +137,24 @@ public SchemaPlus register(SchemaPlus parent, FrameworkConfig frameworkCfg) {

return newSchema;
}

/** */
private static boolean sameParameters(
List<FunctionParameter> params,
List<FunctionParameter> existingParams,
IgniteTypeFactory typeFactory
) {
if (params.size() != existingParams.size())
return false;

for (int i = 0; i < params.size(); ++i) {
RelDataType paramType = typeFactory.toSql(params.get(i).getType(typeFactory));
RelDataType existingParamType = typeFactory.toSql(existingParams.get(i).getType(typeFactory));

if (!paramType.equalsSansFieldNamesAndNullability(existingParamType))
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,27 @@ public void testSameSignatureNotRegistered() throws Exception {
assertEquals(1, schema.getFunctions("SAMESIGN").size());
}

/** */
@Test
public void testOverloadedFunctions() {
client.getOrCreateCache(new CacheConfiguration<Integer, Object>("overloaded-functions")
.setSqlSchema("UDF")
.setSqlFunctionClasses(OverloadedFunctionsLibrary.class));

SchemaPlus schema = queryProcessor(client).schemaHolder().schema("UDF");

assertEquals(2, schema.getFunctions("OVERLOADED").size());
assertEquals(2, schema.getFunctions("OVERLOADED_TABLE").size());
assertEquals(1, schema.getFunctions("SQL_EQUIVALENT").size());

@zstan zstan Sep 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder - why ? Behavior is different from java - is it ok ? It can confuse and brings, as minimal, unexpected behavior ? If it calcite related design - plz show me the related links ? And as minimal - i expect it need to be documented somehow ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This behavior intentionally differs from Java because overload resolution uses SQL types. Both int and Integer correspond to SQL INTEGER, so Calcite cannot reliably distinguish these overloads and their resolution would depend on registration order.
Therefore, overloads must have different SQL parameter types or parameter order. I’ve also documented this behavior in QuerySqlFunction and QuerySqlTableFunction.

assertEquals(1, schema.getFunctions("SQL_EQUIVALENT_TABLE").size());

assertQuery("SELECT UDF.OVERLOADED(1, 'a')").returns("1a").check();
assertQuery("SELECT UDF.OVERLOADED('a', 1)").returns("a1").check();

assertQuery("SELECT * FROM TABLE(UDF.OVERLOADED_TABLE(1, 'a'))").returns("1a").check();
assertQuery("SELECT * FROM TABLE(UDF.OVERLOADED_TABLE('a', 1))").returns("a1").check();
}

/** */
@Test
public void testSystemFunctionOverriding() throws Exception {
Expand Down Expand Up @@ -829,4 +850,55 @@ private static class CustomClass {
return "CustomClass.toString";
}
}

/** */
public static class OverloadedFunctionsLibrary {
/** */
@QuerySqlFunction
public static String overloaded(int i, String s) {
return i + s;
}

/** */
@QuerySqlFunction
public static String overloaded(String s, int i) {
return s + i;
}

/** */
@QuerySqlFunction(alias = "SQL_EQUIVALENT")
public static String sqlEquivalent(int i) {
return String.valueOf(i);
}

/** */
@QuerySqlFunction(alias = "SQL_EQUIVALENT")
public static String sqlEquivalent(Integer i) {
return String.valueOf(i);
}

/** */
@QuerySqlTableFunction(alias = "OVERLOADED_TABLE", columnTypes = {String.class}, columnNames = {"RESULT"})
public static Iterable<Collection<?>> overloadedTable(int i, String s) {
return List.of(List.of(i + s));
}

/** */
@QuerySqlTableFunction(alias = "OVERLOADED_TABLE", columnTypes = {String.class}, columnNames = {"RESULT"})
public static Iterable<Collection<?>> overloadedTable(String s, int i) {
return List.of(List.of(s + i));
}

/** */
@QuerySqlTableFunction(alias = "SQL_EQUIVALENT_TABLE", columnTypes = {String.class}, columnNames = {"RESULT"})
public static Iterable<Collection<?>> sqlEquivalentTable(int i) {
return List.of(List.of(String.valueOf(i)));
}

/** */
@QuerySqlTableFunction(alias = "SQL_EQUIVALENT_TABLE", columnTypes = {String.class}, columnNames = {"RESULT"})
public static Iterable<Collection<?>> sqlEquivalentTable(Integer i) {
return List.of(List.of(String.valueOf(i)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
* cache.query(new SqlFieldsQuery("select sqr(2) where sqr(1) = 1"));
* </pre>
* <p>
* Functions can be overloaded by SQL parameter types or their order. Java types mapped to the same SQL type cannot
* define separate overloads; for example, {@code int} and {@link Integer} both correspond to SQL {@code INTEGER}.
* <p>
* SQL functions can use attributes set on client side:
* <pre name="code" class="java">
* public class MyFunctions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@
* by an {@code 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.
* <p>
* Table functions can be overloaded by SQL parameter types or their order. Java types mapped to the same SQL type
* cannot define separate overloads; for example, {@code int} and {@link Integer} both correspond to SQL
* {@code INTEGER}.
* <p>
* Note, the table functions are available currently only with Calcite.
*
* @see QuerySqlFunction
Expand Down
Loading