Skip to content

Commit 2c4c401

Browse files
committed
Fix Janino classloader issue when analytics-engine is parent plugin
Calcite's EnumerableInterpretable.getBindable() hardcodes EnumerableInterpretable.class.getClassLoader() for Janino compilation. When analytics-engine is the parent classloader via extendedPlugins, this returns the parent classloader which cannot see SQL plugin classes, causing CompileException for any Enumerable code generation. Override implement() in OpenSearchCalcitePreparingStmt to use our own compileWithPluginClassLoader() which does the same code generation but uses CalciteToolsHelper.class.getClassLoader() (SQL plugin's child classloader) so Janino can resolve both parent and child classes. Signed-off-by: Kai Huang <ahkcs@amazon.com>
1 parent 62578cd commit 2c4c401

File tree

2 files changed

+171
-3
lines changed

2 files changed

+171
-3
lines changed

core/src/main/java/org/opensearch/sql/calcite/utils/CalciteToolsHelper.java

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,143 @@ public Type getElementType() {
358358
}
359359
};
360360
}
361-
return super.implement(root);
361+
return implementEnumerable(root);
362+
}
363+
364+
/**
365+
* Implements the Enumerable path with classloader fix for Janino compilation. Calcite's {@code
366+
* EnumerableInterpretable.getBindable()} hardcodes {@code
367+
* EnumerableInterpretable.class.getClassLoader()} as the parent classloader for Janino. When
368+
* analytics-engine is the parent classloader (via extendedPlugins), this returns the
369+
* analytics-engine classloader which cannot see SQL plugin classes. This method replicates the
370+
* Calcite implementation but uses this class's classloader (SQL plugin, child) which can see
371+
* both parent and child classes.
372+
*/
373+
private PreparedResult implementEnumerable(RelRoot root) {
374+
Hook.PLAN_BEFORE_IMPLEMENTATION.run(root);
375+
RelDataType resultType = root.rel.getRowType();
376+
boolean isDml = root.kind.belongsTo(SqlKind.DML);
377+
EnumerableRel enumerable = (EnumerableRel) root.rel;
378+
379+
if (!root.isRefTrivial()) {
380+
List<RexNode> projects = new java.util.ArrayList<>();
381+
final RexBuilder rexBuilder = enumerable.getCluster().getRexBuilder();
382+
for (java.util.Map.Entry<Integer, String> field : root.fields) {
383+
projects.add(rexBuilder.makeInputRef(enumerable, field.getKey()));
384+
}
385+
org.apache.calcite.rex.RexProgram program =
386+
org.apache.calcite.rex.RexProgram.create(
387+
enumerable.getRowType(), projects, null, root.validatedRowType, rexBuilder);
388+
enumerable =
389+
org.apache.calcite.adapter.enumerable.EnumerableCalc.create(enumerable, program);
390+
}
391+
392+
// Access the internalParameters map via reflection. This map is shared with the
393+
// DataContext so stashed values (e.g., table scan references) are available at execution.
394+
java.util.Map<String, Object> parameters;
395+
try {
396+
java.lang.reflect.Field f =
397+
CalcitePrepareImpl.CalcitePreparingStmt.class.getDeclaredField("internalParameters");
398+
f.setAccessible(true);
399+
@SuppressWarnings("unchecked")
400+
java.util.Map<String, Object> p = (java.util.Map<String, Object>) f.get(this);
401+
parameters = p;
402+
} catch (ReflectiveOperationException e) {
403+
throw new RuntimeException("Failed to access internalParameters", e);
404+
}
405+
406+
// Match original CalcitePreparingStmt.implement() which puts _conformance before toBindable
407+
parameters.put("_conformance", context.config().conformance());
408+
409+
CatalogReader.THREAD_LOCAL.set(catalogReader);
410+
final Bindable bindable;
411+
try {
412+
bindable = compileWithPluginClassLoader(enumerable, parameters);
413+
} finally {
414+
CatalogReader.THREAD_LOCAL.remove();
415+
}
416+
417+
return new PreparedResultImpl(
418+
resultType,
419+
requireNonNull(parameterRowType, "parameterRowType"),
420+
requireNonNull(fieldOrigins, "fieldOrigins"),
421+
root.collation.getFieldCollations().isEmpty()
422+
? ImmutableList.of()
423+
: ImmutableList.of(root.collation),
424+
root.rel,
425+
mapTableModOp(isDml, root.kind),
426+
isDml) {
427+
@Override
428+
public String getCode() {
429+
throw new UnsupportedOperationException();
430+
}
431+
432+
@Override
433+
public Bindable getBindable(Meta.CursorFactory cursorFactory) {
434+
return bindable;
435+
}
436+
437+
@Override
438+
public Type getElementType() {
439+
return resultType.getFieldList().size() == 1 ? Object.class : Object[].class;
440+
}
441+
};
442+
}
443+
444+
/**
445+
* Compiles an EnumerableRel to a Bindable using the SQL plugin's classloader. This is
446+
* equivalent to {@code EnumerableInterpretable.toBindable()} + {@code getBindable()} but uses
447+
* this class's classloader instead of {@code EnumerableInterpretable.class.getClassLoader()}.
448+
*/
449+
private static Bindable compileWithPluginClassLoader(
450+
EnumerableRel rel, java.util.Map<String, Object> parameters) {
451+
try {
452+
org.apache.calcite.adapter.enumerable.EnumerableRelImplementor relImplementor =
453+
new org.apache.calcite.adapter.enumerable.EnumerableRelImplementor(
454+
rel.getCluster().getRexBuilder(), parameters);
455+
org.apache.calcite.linq4j.tree.ClassDeclaration expr =
456+
relImplementor.implementRoot(rel, EnumerableRel.Prefer.ARRAY);
457+
String s =
458+
org.apache.calcite.linq4j.tree.Expressions.toString(
459+
expr.memberDeclarations, "\n", false);
460+
Hook.JAVA_PLAN.run(s);
461+
462+
// Use this class's classloader (SQL plugin) instead of
463+
// EnumerableInterpretable.class.getClassLoader() (analytics-engine parent).
464+
// commons-compiler is in the parent classloader at runtime, so we use reflection.
465+
ClassLoader classLoader = CalciteToolsHelper.class.getClassLoader();
466+
Class<?> factoryFactoryClass =
467+
classLoader.loadClass("org.codehaus.commons.compiler.CompilerFactoryFactory");
468+
Object compilerFactory =
469+
factoryFactoryClass
470+
.getMethod("getDefaultCompilerFactory", ClassLoader.class)
471+
.invoke(null, classLoader);
472+
Object compiler =
473+
compilerFactory.getClass().getMethod("newSimpleCompiler").invoke(compilerFactory);
474+
compiler
475+
.getClass()
476+
.getMethod("setParentClassLoader", ClassLoader.class)
477+
.invoke(compiler, classLoader);
478+
479+
String fullCode =
480+
"public final class "
481+
+ expr.name
482+
+ " implements "
483+
+ Bindable.class.getName()
484+
+ ", "
485+
+ org.apache.calcite.runtime.Typed.class.getName()
486+
+ " {\n"
487+
+ s
488+
+ "\n}\n";
489+
compiler.getClass().getMethod("cook", String.class).invoke(compiler, fullCode);
490+
ClassLoader compiledClassLoader =
491+
(ClassLoader) compiler.getClass().getMethod("getClassLoader").invoke(compiler);
492+
@SuppressWarnings("unchecked")
493+
Class<Bindable> clazz = (Class<Bindable>) compiledClassLoader.loadClass(expr.name);
494+
return clazz.getDeclaredConstructors()[0].newInstance() instanceof Bindable b ? b : null;
495+
} catch (Exception e) {
496+
throw org.apache.calcite.util.Util.throwAsRuntime(e);
497+
}
362498
}
363499

364500
@Override

opensearch/src/main/java/org/opensearch/sql/opensearch/storage/script/CalciteScriptEngine.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ public <T> T compile(
137137
getter,
138138
new RelRecordType(List.of()));
139139

140-
Function1<DataContext, Object[]> function =
141-
new RexExecutable(code, "generated Rex code").getFunction();
140+
Function1<DataContext, Object[]> function = compileRexCode(code);
142141

143142
if (CONTEXTS.containsKey(context)) {
144143
return context.factoryClazz.cast(CONTEXTS.get(context).apply(function, rexNode.getType()));
@@ -150,6 +149,39 @@ public <T> T compile(
150149
CONTEXTS, context));
151150
}
152151

152+
/**
153+
* Compile Rex code using the SQL plugin's classloader. Calcite's {@link RexExecutable} hardcodes
154+
* {@code RexExecutable.class.getClassLoader()} for Janino, which returns the analytics-engine
155+
* parent classloader when extendedPlugins is used. This method uses this class's classloader (SQL
156+
* plugin) so Janino can resolve both parent and child classes.
157+
*/
158+
@SuppressWarnings("unchecked")
159+
private static Function1<DataContext, Object[]> compileRexCode(String code) {
160+
try {
161+
// Use reflection for Janino classes (available at runtime via parent classloader,
162+
// not on opensearch module compile classpath).
163+
ClassLoader classLoader = CalciteScriptEngine.class.getClassLoader();
164+
Class<?> cbeClass = classLoader.loadClass("org.codehaus.janino.ClassBodyEvaluator");
165+
Object cbe = cbeClass.getDeclaredConstructor().newInstance();
166+
cbeClass.getMethod("setClassName", String.class).invoke(cbe, "Reducer");
167+
cbeClass
168+
.getMethod("setExtendedClass", Class.class)
169+
.invoke(cbe, org.apache.calcite.runtime.Utilities.class);
170+
cbeClass
171+
.getMethod("setImplementedInterfaces", Class[].class)
172+
.invoke(cbe, (Object) new Class[] {Function1.class, java.io.Serializable.class});
173+
cbeClass.getMethod("setParentClassLoader", ClassLoader.class).invoke(cbe, classLoader);
174+
175+
// ClassBodyEvaluator.cook(String) compiles the source code
176+
cbeClass.getMethod("cook", String.class).invoke(cbe, code);
177+
178+
Class<?> clazz = (Class<?>) cbeClass.getMethod("getClazz").invoke(cbe);
179+
return (Function1<DataContext, Object[]>) clazz.getDeclaredConstructor().newInstance();
180+
} catch (Exception e) {
181+
throw Util.throwAsRuntime(e);
182+
}
183+
}
184+
153185
@Override
154186
public Set<ScriptContext<?>> getSupportedContexts() {
155187
return CONTEXTS.keySet();

0 commit comments

Comments
 (0)