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
Binary file modified libs/analytics-engine-3.7.0-SNAPSHOT.zip
Binary file not shown.
Binary file modified libs/analytics-framework-3.7.0-SNAPSHOT.jar
Binary file not shown.
54 changes: 11 additions & 43 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,53 +55,21 @@ opensearchplugin {
name 'opensearch-sql'
description 'OpenSearch SQL'
classname 'org.opensearch.sql.plugin.SQLPlugin'
extendedPlugins = ['opensearch-job-scheduler', 'analytics-engine']
extendedPlugins = ['opensearch-job-scheduler', 'analytics-engine;optional=true']
licenseFile rootProject.file("LICENSE.txt")
noticeFile rootProject.file("NOTICE")
}

// Exclude jars provided by the analytics-engine plugin (shared via extendedPlugins classloader).
// MUST match what's actually in the analytics-engine ZIP to avoid both jar hell (when analytics-engine
// ships it) and ClassNotFoundException at runtime (when it doesn't). Last verified against
// analytics-engine-3.7.0-SNAPSHOT — keep this list aligned when bumping versions.
bundlePlugin {
exclude 'calcite-core-*.jar'
exclude 'calcite-linq4j-*.jar'
exclude 'avatica-core-*.jar'
exclude 'guava-*.jar'
exclude 'failureaccess-*.jar'
exclude 'slf4j-api-*.jar'
exclude 'commons-codec-*.jar'
exclude 'commons-compiler-*.jar'
exclude 'commons-io-*.jar'
exclude 'commons-lang3-*.jar'
exclude 'janino-*.jar'
exclude 'joou-java-6-*.jar'
exclude 'json-path-*.jar'
exclude 'jackson-annotations-*.jar'
exclude 'jackson-databind-*.jar'
exclude 'httpcore5-5*.jar'
// TODO: Remove the three httpcore5/httpclient5 exclusions below — and ideally this entire
// bundlePlugin exclusion block — once analytics-engine becomes an optional dependency via the
// AnalyticsFrontEndExtension SPI (opensearch-project/OpenSearch#21449). The shared-classloader
// jar deduplication that requires this hand-maintained list goes away with the SPI.
exclude 'httpcore5-h2-*.jar'
exclude 'httpcore5-reactive-*.jar'
exclude 'httpclient5-*.jar'
exclude 'commons-text-*.jar'
// Calcite transitive deps now bundled in analytics-engine 3.7 — exclude from sql to avoid jar hell.
exclude 'commons-math3-*.jar'
exclude 'commons-dbcp2-*.jar'
exclude 'commons-pool2-*.jar'
exclude 'uzaygezen-core-*.jar'
exclude 'sketches-core-*.jar'
exclude 'memory-0*.jar'
exclude 'jts-io-common-*.jar'
exclude 'proj4j-*.jar'
exclude 'json-smart-*.jar'
exclude 'accessors-smart-*.jar'
exclude 'asm-9*.jar'
}
// SQL bundles its own copies of all transitive deps (Calcite, Guava, etc.) so the plugin can
// boot and serve queries when analytics-engine is not installed. With ';optional=true',
// PluginsService.checkBundleJarHell skips both URL-overlap and class-level checks against
// analytics-engine (server/.../PluginsService.java:763-765), so duplicate bundling is safe.
//
// At runtime: when analytics-engine IS installed, parent-first classloader delegation gives
// SQL analytics-engine's patched Calcite (CALCITE-3745) and shared deps; SQL's own bundled
// copies are shadowed. When analytics-engine is absent, SQL falls back to its own bundled
// copies — Janino's classloader story works because everything lives in SQL's classloader,
// no parent visibility gap to bridge.

publishing {
publications {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.plugin;

import org.opensearch.analytics.spi.AnalyticsFrontEndExtension;
import org.opensearch.analytics.spi.AnalyticsServices;
import org.opensearch.sql.plugin.rest.AnalyticsExecutorHolder;

/**
* SPI consumer that publishes the analytics-engine services into {@link AnalyticsExecutorHolder}
* when analytics-engine is installed.
*
* <p>Kept separate from {@link SQLPlugin} so that SQLPlugin's bytecode does not reference any
* analytics-framework class. When analytics-engine is absent, OpenSearch never invokes {@code
* ServiceLoader.load(AnalyticsFrontEndExtension.class)} (because no plugin defining the SPI is
* present), so this class is never resolved and SQL boots without needing analytics-framework on
* its runtime classpath.
*/
public class SQLAnalyticsFrontEndExtension implements AnalyticsFrontEndExtension {

@Override
public void setAnalyticsServices(AnalyticsServices services) {
AnalyticsExecutorHolder.set(services.queryPlanExecutor(), services.schemaProvider());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,14 @@ private BiFunction<SQLQueryRequest, RestChannel, Boolean> createSqlAnalyticsRout
java.util.function.Supplier<RestUnifiedQueryAction> handlerSupplier =
() -> {
if (cached[0] == null) {
var executor = AnalyticsExecutorHolder.get();
if (executor == null) {
Object executor = AnalyticsExecutorHolder.getQueryPlanExecutor();
Object schemaProvider = AnalyticsExecutorHolder.getSchemaProvider();
if (executor == null || schemaProvider == null) {
return null;
}
cached[0] = new RestUnifiedQueryAction(client, clusterService, executor);
cached[0] =
RestUnifiedQueryAction.fromUnknownExecutor(
client, clusterService, executor, schemaProvider);
}
return cached[0];
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,48 @@

package org.opensearch.sql.plugin.rest;

import org.apache.calcite.rel.RelNode;
import org.opensearch.analytics.exec.QueryPlanExecutor;

/**
* Bridge for sharing the analytics-engine {@link QueryPlanExecutor} between the PPL transport
* action (where Guice resolves the binding via {@code @Inject}) and the REST-only SQL router (where
* Guice cannot, because {@code SQLPlugin#getRestHandlers} runs before the Node-level injector
* satisfies {@code @Inject} parameters).
* Static bridge that lets {@code SQLAnalyticsFrontEndExtension} (the SPI consumer) hand off the
* analytics-engine services to the SQL request paths that need them.
*
* <p>Why a static holder: cross-plugin Guice injection needs a class registered in the Node
* injector, and {@link org.opensearch.sql.plugin.SQLPlugin}'s SQL routing path is built in {@code
* getRestHandlers} — outside any Guice-managed lifecycle. Persisting the executor in this holder
* once {@link org.opensearch.sql.plugin.transport.TransportPPLQueryAction} is constructed lets the
* SQL router read the same instance without going back through the injector.
* <p>Stored as {@link Object} on purpose. Callers cast at use sites that are already gated on a
* non-null value, ensuring no analytics-framework class is referenced from any signature loaded at
* SQL plugin startup. When analytics-engine is not installed, the SPI never fires, the holder stays
* null, and {@code TransportPPLQueryAction} / {@code SQLPlugin#createSqlAnalyticsRouter} fall
* through to the legacy paths without ever touching analytics-framework types.
*/
public final class AnalyticsExecutorHolder {

private static volatile QueryPlanExecutor<RelNode, Iterable<Object[]>> executor;
private static volatile Object queryPlanExecutor;
private static volatile Object schemaProvider;

private AnalyticsExecutorHolder() {}

public static void set(QueryPlanExecutor<RelNode, Iterable<Object[]>> instance) {
executor = instance;
/**
* Set both services in one call. Invoked by {@code SQLAnalyticsFrontEndExtension} from the SPI
* push lifecycle. Either argument may be {@code null} (not expected today, but tolerated).
*/
public static void set(Object queryPlanExecutor, Object schemaProvider) {
AnalyticsExecutorHolder.queryPlanExecutor = queryPlanExecutor;
AnalyticsExecutorHolder.schemaProvider = schemaProvider;
}

/**
* Returns the analytics-engine query plan executor as {@link Object}. Returns {@code null} when
* analytics-engine is not installed (SPI never fired). Callers cast to {@code
* QueryPlanExecutor<RelNode, Iterable<Object[]>>} only inside code paths that are gated on a
* non-null return value — see {@code RestUnifiedQueryAction#fromUnknownExecutor}.
*/
public static Object getQueryPlanExecutor() {
return queryPlanExecutor;
}

public static QueryPlanExecutor<RelNode, Iterable<Object[]>> get() {
return executor;
/**
* Returns the analytics-engine schema provider as {@link Object}. Returns {@code null} when
* analytics-engine is not installed. Callers cast to {@code SchemaProvider} only inside code
* paths that are gated on a non-null return value.
*/
public static Object getSchemaProvider() {
return schemaProvider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.ThreadContext;
import org.opensearch.analytics.exec.QueryPlanExecutor;
import org.opensearch.analytics.schema.OpenSearchSchemaBuilder;
import org.opensearch.analytics.schema.SchemaProvider;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.unit.TimeValue;
import org.opensearch.core.action.ActionListener;
Expand Down Expand Up @@ -59,14 +59,33 @@ public class RestUnifiedQueryAction {
private final AnalyticsExecutionEngine analyticsEngine;
private final NodeClient client;
private final ClusterService clusterService;
private final SchemaProvider schemaProvider;

public RestUnifiedQueryAction(
NodeClient client,
ClusterService clusterService,
QueryPlanExecutor<RelNode, Iterable<Object[]>> planExecutor) {
QueryPlanExecutor<RelNode, Iterable<Object[]>> planExecutor,
SchemaProvider schemaProvider) {
this.client = client;
this.clusterService = clusterService;
this.analyticsEngine = new AnalyticsExecutionEngine(planExecutor);
this.schemaProvider = schemaProvider;
}

/**
* Construct from holder-stashed services typed as {@link Object} so callers don't take a
* compile-time reference on analytics-framework. The cast is confined to this method — invoking
* it loads the analytics-framework types for the first time, and the caller must gate on a
* non-null executor (i.e. analytics-engine is installed).
*/
@SuppressWarnings("unchecked")
public static RestUnifiedQueryAction fromUnknownExecutor(
NodeClient client, ClusterService clusterService, Object executor, Object schemaProvider) {
return new RestUnifiedQueryAction(
client,
clusterService,
(QueryPlanExecutor<RelNode, Iterable<Object[]>>) executor,
(SchemaProvider) schemaProvider);
}

/**
Expand Down Expand Up @@ -161,7 +180,7 @@ private static UnifiedQueryContext buildParsingContext(QueryType queryType) {
private UnifiedQueryContext buildContext(QueryType queryType, boolean profiling) {
return UnifiedQueryContext.builder()
.language(queryType)
.catalog(SCHEMA_NAME, OpenSearchSchemaBuilder.buildSchema(clusterService.state()))
.catalog(SCHEMA_NAME, schemaProvider.buildSchema(clusterService.state()))
.defaultNamespace(SCHEMA_NAME)
.profiling(profiling)
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,9 @@
import java.util.Locale;
import java.util.Optional;
import java.util.function.Supplier;
import org.apache.calcite.rel.RelNode;
import org.opensearch.action.ActionRequest;
import org.opensearch.action.support.ActionFilters;
import org.opensearch.action.support.HandledTransportAction;
import org.opensearch.analytics.exec.QueryPlanExecutor;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.common.inject.Guice;
import org.opensearch.common.inject.Inject;
Expand Down Expand Up @@ -62,7 +60,16 @@ public class TransportPPLQueryAction

private final Supplier<Boolean> pplEnabled;

private final RestUnifiedQueryAction unifiedQueryHandler;
private final NodeClient client;

private final ClusterService clusterService;

/**
* Lazily-resolved analytics handler. {@code null} until the first analytics-routed request, and
* remains {@code null} forever if analytics-engine is not installed (the SPI never fires, so
* {@link AnalyticsExecutorHolder#getQueryPlanExecutor()} stays null).
*/
private volatile RestUnifiedQueryAction unifiedQueryHandler;

/** Constructor of TransportPPLQueryAction. */
@Inject
Expand All @@ -72,10 +79,12 @@ public TransportPPLQueryAction(
NodeClient client,
ClusterService clusterService,
DataSourceServiceImpl dataSourceService,
org.opensearch.common.settings.Settings clusterSettings,
QueryPlanExecutor<RelNode, Iterable<Object[]>> queryPlanExecutor) {
org.opensearch.common.settings.Settings clusterSettings) {
super(PPLQueryAction.NAME, transportService, actionFilters, TransportPPLQueryRequest::new);

this.client = client;
this.clusterService = clusterService;

ModulesBuilder modules = new ModulesBuilder();
modules.add(new OpenSearchPluginModule());
modules.add(
Expand All @@ -86,9 +95,6 @@ public TransportPPLQueryAction(
b.bind(DataSourceService.class).toInstance(dataSourceService);
});
this.injector = Guice.createInjector(modules);
AnalyticsExecutorHolder.set(queryPlanExecutor);
this.unifiedQueryHandler =
new RestUnifiedQueryAction(client, clusterService, queryPlanExecutor);
this.pplEnabled =
() ->
MULTI_ALLOW_EXPLICIT_INDEX.get(clusterSettings)
Expand All @@ -98,6 +104,31 @@ public TransportPPLQueryAction(
.getSettingValue(Settings.Key.PPL_ENABLED);
}

/**
* Resolves the analytics-engine-backed handler lazily. Returns {@code null} when analytics-engine
* is not installed; callers fall through to the legacy PPL pipeline. Synchronized double-checked
* cache so we only build the handler once on the first analytics request.
*/
private RestUnifiedQueryAction analyticsHandler() {
RestUnifiedQueryAction cached = unifiedQueryHandler;
if (cached != null) {
return cached;
}
Object executor = AnalyticsExecutorHolder.getQueryPlanExecutor();
Object schemaProvider = AnalyticsExecutorHolder.getSchemaProvider();
if (executor == null || schemaProvider == null) {
return null;
}
synchronized (this) {
if (unifiedQueryHandler == null) {
unifiedQueryHandler =
RestUnifiedQueryAction.fromUnknownExecutor(
client, clusterService, executor, schemaProvider);
}
return unifiedQueryHandler;
}
}

/**
* {@inheritDoc} Transform the request and call super.doExecute() to support call from other
* plugins.
Expand Down Expand Up @@ -134,16 +165,20 @@ protected void doExecute(
QueryContext.setProfile(transformedRequest.profile());
ActionListener<TransportPPLQueryResponse> clearingListener = wrapWithProfilingClear(listener);

// Route to analytics engine for non-Lucene (e.g., Parquet-backed) indices
if (unifiedQueryHandler.isAnalyticsIndex(transformedRequest.getRequest(), QueryType.PPL)) {
// Route to analytics engine for non-Lucene (e.g., Parquet-backed) indices.
// analyticsHandler() returns null when analytics-engine isn't installed — we fall through
// to the regular Lucene PPL path so non-analytics queries still work.
RestUnifiedQueryAction analytics = analyticsHandler();
if (analytics != null
&& analytics.isAnalyticsIndex(transformedRequest.getRequest(), QueryType.PPL)) {
if (transformedRequest.isExplainRequest()) {
unifiedQueryHandler.explain(
analytics.explain(
transformedRequest.getRequest(),
QueryType.PPL,
transformedRequest.mode(),
createExplainResponseListener(transformedRequest, clearingListener));
} else {
unifiedQueryHandler.execute(
analytics.execute(
transformedRequest.getRequest(),
QueryType.PPL,
transformedRequest.profile(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#
# Copyright OpenSearch Contributors
# SPDX-License-Identifier: Apache-2.0
#

org.opensearch.sql.plugin.SQLAnalyticsFrontEndExtension
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.junit.Before;
import org.junit.Test;
import org.opensearch.analytics.exec.QueryPlanExecutor;
import org.opensearch.analytics.schema.SchemaProvider;
import org.opensearch.cluster.service.ClusterService;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.transport.client.node.NodeClient;
Expand All @@ -30,7 +31,11 @@ public void setUp() {
@SuppressWarnings("unchecked")
QueryPlanExecutor<RelNode, Iterable<Object[]>> executor = mock(QueryPlanExecutor.class);
action =
new RestUnifiedQueryAction(mock(NodeClient.class), mock(ClusterService.class), executor);
new RestUnifiedQueryAction(
mock(NodeClient.class),
mock(ClusterService.class),
executor,
mock(SchemaProvider.class));
}

@Test
Expand Down
Loading