diff --git a/core/src/main/java/org/apache/calcite/model/ModelHandler.java b/core/src/main/java/org/apache/calcite/model/ModelHandler.java index c30cfc2815b..d9b064392af 100644 --- a/core/src/main/java/org/apache/calcite/model/ModelHandler.java +++ b/core/src/main/java/org/apache/calcite/model/ModelHandler.java @@ -227,10 +227,10 @@ public static void addFunctions(ClassNameFilter filter, SchemaPlus schema, filter.check(className); final Class clazz; try { - clazz = Class.forName(className); + // Defer class initialization until it is checked to be a valid function (see below) + clazz = Class.forName(className, false, ModelHandler.class.getClassLoader()); } catch (ClassNotFoundException e) { - throw new RuntimeException("UDF class '" - + className + "' not found"); + throw new RuntimeException("UDF class '" + className + "' not found"); } String methodNameOrDefault = Util.first(methodName, "eval"); String actualFunctionName; diff --git a/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java b/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java index 7d0e00eda2b..258c0d8bf07 100644 --- a/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java +++ b/core/src/test/java/org/apache/calcite/model/ModelHandlerTest.java @@ -36,6 +36,7 @@ import java.sql.DriverManager; import java.util.Properties; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; @@ -120,6 +121,33 @@ public class ModelHandlerTest { assertThat(e.getMessage(), containsString("javax.naming.")); } + /** Set by {@link NotAFunction}'s static initializer. */ + static final AtomicBoolean NOT_A_FUNCTION_INITIALIZED = new AtomicBoolean(false); + + /** Not a valid function class; records whether its static initializer ran. */ + public static class NotAFunction { + static { + NOT_A_FUNCTION_INITIALIZED.set(true); + } + } + + /** Test case for + * [CALCITE-7772] ModelHandler + * addFunctions should defer UDF class initialization until after shape validation. + * A class that passes the name filter but fails the function-shape + * checks must be rejected without its static initializer running. */ + @Test void testRejectedUdfClassIsNotInitialized() { + SchemaPlus root = CalciteSchema.createRootSchema(false, false).plus(); + ClassNameFilter permissive = ClassNameFilter.of("", "org.apache.calcite."); + RuntimeException e = + assertThrows(RuntimeException.class, () -> + ModelHandler.addFunctions(permissive, root, "f", + NotAFunction.class.getName(), null, false)); + assertThat(e.getMessage(), containsString("Not a valid function class")); + assertThat("static initializer of a rejected UDF class must not run", + NOT_A_FUNCTION_INITIALIZED.get(), is(false)); + } + @Test void testDenyFactory() { String model = "inline:{" + " version: '1.0',"