Skip to content

Commit ca8bcf1

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 ca8bcf1

1 file changed

Lines changed: 134 additions & 1 deletion

File tree

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

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,140 @@ 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+
CatalogReader.THREAD_LOCAL.set(catalogReader);
407+
final Bindable bindable;
408+
try {
409+
bindable = compileWithPluginClassLoader(enumerable, parameters);
410+
} finally {
411+
CatalogReader.THREAD_LOCAL.remove();
412+
}
413+
414+
return new PreparedResultImpl(
415+
resultType,
416+
requireNonNull(parameterRowType, "parameterRowType"),
417+
requireNonNull(fieldOrigins, "fieldOrigins"),
418+
root.collation.getFieldCollations().isEmpty()
419+
? ImmutableList.of()
420+
: ImmutableList.of(root.collation),
421+
root.rel,
422+
mapTableModOp(isDml, root.kind),
423+
isDml) {
424+
@Override
425+
public String getCode() {
426+
throw new UnsupportedOperationException();
427+
}
428+
429+
@Override
430+
public Bindable getBindable(Meta.CursorFactory cursorFactory) {
431+
return bindable;
432+
}
433+
434+
@Override
435+
public Type getElementType() {
436+
return resultType.getFieldList().size() == 1 ? Object.class : Object[].class;
437+
}
438+
};
439+
}
440+
441+
/**
442+
* Compiles an EnumerableRel to a Bindable using the SQL plugin's classloader. This is
443+
* equivalent to {@code EnumerableInterpretable.toBindable()} + {@code getBindable()} but uses
444+
* this class's classloader instead of {@code EnumerableInterpretable.class.getClassLoader()}.
445+
*/
446+
private static Bindable compileWithPluginClassLoader(
447+
EnumerableRel rel, java.util.Map<String, Object> parameters) {
448+
try {
449+
org.apache.calcite.adapter.enumerable.EnumerableRelImplementor relImplementor =
450+
new org.apache.calcite.adapter.enumerable.EnumerableRelImplementor(
451+
rel.getCluster().getRexBuilder(), parameters);
452+
org.apache.calcite.linq4j.tree.ClassDeclaration expr =
453+
relImplementor.implementRoot(rel, EnumerableRel.Prefer.ARRAY);
454+
String s =
455+
org.apache.calcite.linq4j.tree.Expressions.toString(
456+
expr.memberDeclarations, "\n", false);
457+
Hook.JAVA_PLAN.run(s);
458+
459+
// Use this class's classloader (SQL plugin) instead of
460+
// EnumerableInterpretable.class.getClassLoader() (analytics-engine parent).
461+
// commons-compiler is in the parent classloader at runtime, so we use reflection.
462+
ClassLoader classLoader = CalciteToolsHelper.class.getClassLoader();
463+
Class<?> factoryFactoryClass =
464+
classLoader.loadClass("org.codehaus.commons.compiler.CompilerFactoryFactory");
465+
Object compilerFactory =
466+
factoryFactoryClass
467+
.getMethod("getDefaultCompilerFactory", ClassLoader.class)
468+
.invoke(null, classLoader);
469+
Object compiler =
470+
compilerFactory.getClass().getMethod("newSimpleCompiler").invoke(compilerFactory);
471+
compiler
472+
.getClass()
473+
.getMethod("setParentClassLoader", ClassLoader.class)
474+
.invoke(compiler, classLoader);
475+
476+
String fullCode =
477+
"public final class "
478+
+ expr.name
479+
+ " implements "
480+
+ Bindable.class.getName()
481+
+ ", "
482+
+ org.apache.calcite.runtime.Typed.class.getName()
483+
+ " {\n"
484+
+ s
485+
+ "\n}\n";
486+
compiler.getClass().getMethod("cook", String.class).invoke(compiler, fullCode);
487+
ClassLoader compiledClassLoader =
488+
(ClassLoader) compiler.getClass().getMethod("getClassLoader").invoke(compiler);
489+
@SuppressWarnings("unchecked")
490+
Class<Bindable> clazz = (Class<Bindable>) compiledClassLoader.loadClass(expr.name);
491+
return clazz.getDeclaredConstructors()[0].newInstance() instanceof Bindable b ? b : null;
492+
} catch (Exception e) {
493+
throw org.apache.calcite.util.Util.throwAsRuntime(e);
494+
}
362495
}
363496

364497
@Override

0 commit comments

Comments
 (0)