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 @@ -9,6 +9,7 @@
import lombok.Getter;
import org.opensearch.sql.ast.AbstractNodeVisitor;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.sql.protocol.response.format.Format;

/** Explain Statement. */
@Getter
Expand All @@ -18,15 +19,21 @@ public class Explain extends Statement {
private final Statement statement;
private final QueryType queryType;
private final ExplainMode mode;
private final Format format;

public Explain(Statement statement, QueryType queryType) {
this(statement, queryType, null);
this(statement, queryType, null, null);
}

public Explain(Statement statement, QueryType queryType, String mode) {
this(statement, queryType, mode, null);
}

public Explain(Statement statement, QueryType queryType, String mode, Format format) {
this.statement = statement;
this.queryType = queryType;
this.mode = ExplainMode.of(mode);
this.format = format;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ public Sort copy(
@Override
public RelWriter explainTerms(RelWriter pw) {
super.explainTerms(pw);
// Show type in the explain
pw.item("type", type);
// Show type in the explain - convert to string for JSON serialization compatibility
pw.item("type", type.name());
return pw;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.opensearch.sql.data.type.ExprType;
import org.opensearch.sql.executor.pagination.Cursor;
import org.opensearch.sql.planner.physical.PhysicalPlan;
import org.opensearch.sql.protocol.response.format.Format;

/** Execution engine that encapsulates execution details. */
public interface ExecutionEngine {
Expand Down Expand Up @@ -75,6 +76,16 @@ default void explain(
getClass().getSimpleName() + " does not support RelNode explain"));
}

default void explain(
RelNode plan,
ExplainMode mode,
Format format,
CalcitePlanContext context,
ResponseListener<ExplainResponse> listener) {
// Default: ignore format parameter, delegate to old signature for BWC
explain(plan, mode, context, listener);
}

/** Data class that encapsulates ExprValue. */
@Data
class QueryResponse {
Expand Down Expand Up @@ -163,5 +174,8 @@ class ExplainResponseNodeV2 {
private final String logical;
private final String physical;
private final String extended;
// For json_tree format: parsed JSON objects instead of strings
private Object logicalTree;
private Object physicalTree;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
import org.opensearch.sql.planner.logical.LogicalPaginate;
import org.opensearch.sql.planner.logical.LogicalPlan;
import org.opensearch.sql.planner.physical.PhysicalPlan;
import org.opensearch.sql.protocol.response.format.Format;

/** The low level interface of core engine. */
@RequiredArgsConstructor
Expand Down Expand Up @@ -124,8 +125,19 @@ public void explain(
HighlightConfig highlightConfig,
ResponseListener<ExecutionEngine.ExplainResponse> listener,
ExplainMode mode) {
explain(plan, queryType, highlightConfig, listener, mode, null);
}

/** Explain with optional highlight config and format. */
public void explain(
UnresolvedPlan plan,
QueryType queryType,
HighlightConfig highlightConfig,
ResponseListener<ExecutionEngine.ExplainResponse> listener,
ExplainMode mode,
Format format) {
if (shouldUseCalcite(queryType)) {
explainWithCalcite(plan, queryType, highlightConfig, listener, mode);
explainWithCalcite(plan, queryType, highlightConfig, listener, mode, format);
} else {
explainWithLegacy(plan, queryType, listener, mode, Optional.empty());
}
Expand Down Expand Up @@ -192,6 +204,16 @@ public void explainWithCalcite(
HighlightConfig highlightConfig,
ResponseListener<ExecutionEngine.ExplainResponse> listener,
ExplainMode mode) {
explainWithCalcite(plan, queryType, highlightConfig, listener, mode, null);
}

public void explainWithCalcite(
UnresolvedPlan plan,
QueryType queryType,
HighlightConfig highlightConfig,
ResponseListener<ExecutionEngine.ExplainResponse> listener,
ExplainMode mode,
Format format) {
CalcitePlanContext.run(
() -> {
try {
Expand All @@ -206,7 +228,11 @@ public void explainWithCalcite(
() -> {
RelNode relNode = analyze(plan, context);
RelNode calcitePlan = convertToCalcitePlan(relNode, context);
executionEngine.explain(calcitePlan, mode, context, listener);
if (format != null) {
executionEngine.explain(calcitePlan, mode, format, context, listener);
} else {
executionEngine.explain(calcitePlan, mode, context, listener);
}
},
settings);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.opensearch.sql.executor.ExecutionEngine;
import org.opensearch.sql.executor.QueryId;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.sql.protocol.response.format.Format;

/** AbstractPlan represent the execution entity of the Statement. */
@RequiredArgsConstructor
Expand All @@ -32,4 +33,16 @@ public abstract class AbstractPlan {
*/
public abstract void explain(
ResponseListener<ExecutionEngine.ExplainResponse> listener, ExplainMode mode);

/**
* Explain query execution with format.
*
* @param listener query explain response listener.
* @param mode explain mode
* @param format output format
*/
public void explain(
ResponseListener<ExecutionEngine.ExplainResponse> listener, ExplainMode mode, Format format) {
explain(listener, mode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import org.opensearch.sql.executor.ExecutionEngine;
import org.opensearch.sql.executor.QueryId;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.sql.protocol.response.format.Format;

/** Explain plan. */
public class ExplainPlan extends AbstractPlan {

private final AbstractPlan plan;
private final ExplainMode mode;
private final Format format;

private final ResponseListener<ExecutionEngine.ExplainResponse> explainListener;

Expand All @@ -26,20 +28,38 @@ public ExplainPlan(
AbstractPlan plan,
ExplainMode mode,
ResponseListener<ExecutionEngine.ExplainResponse> explainListener) {
this(queryId, queryType, plan, mode, null, explainListener);
}

/** Constructor with format. */
public ExplainPlan(
QueryId queryId,
QueryType queryType,
AbstractPlan plan,
ExplainMode mode,
Format format,
ResponseListener<ExecutionEngine.ExplainResponse> explainListener) {
super(queryId, queryType);
this.plan = plan;
this.mode = mode;
this.format = format;
this.explainListener = explainListener;
}

@Override
public void execute() {
plan.explain(explainListener, mode);
plan.explain(explainListener, mode, format);
}

@Override
public void explain(
ResponseListener<ExecutionEngine.ExplainResponse> listener, ExplainMode mode) {
throw new UnsupportedOperationException("explain query can not been explained.");
}

@Override
public void explain(
ResponseListener<ExecutionEngine.ExplainResponse> listener, ExplainMode mode, Format format) {
throw new UnsupportedOperationException("explain query can not been explained.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.opensearch.sql.executor.QueryId;
import org.opensearch.sql.executor.QueryService;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.sql.protocol.response.format.Format;

/** Query plan which includes a <em>select</em> query. */
public class QueryPlan extends AbstractPlan {
Expand Down Expand Up @@ -86,12 +87,18 @@ public void execute() {
@Override
public void explain(
ResponseListener<ExecutionEngine.ExplainResponse> listener, ExplainMode mode) {
explain(listener, mode, null);
}

@Override
public void explain(
ResponseListener<ExecutionEngine.ExplainResponse> listener, ExplainMode mode, Format format) {
if (pageSize.isPresent()) {
listener.onFailure(
new NotImplementedException(
"`explain` feature for paginated requests is not implemented yet."));
} else {
queryService.explain(plan, getQueryType(), highlightConfig, listener, mode);
queryService.explain(plan, getQueryType(), highlightConfig, listener, mode, format);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public AbstractPlan visitExplain(
node.getQueryType(),
create(node.getStatement(), NO_CONSUMER_RESPONSE_LISTENER, context.getRight()),
node.getMode(),
node.getFormat(),
context.getRight());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public enum Format {
JSON("json"),
/** Returns explain output in yaml format */
YAML("yaml"),
/** Returns explain output as structured JSON tree using RelJsonWriter */
JSON_TREE("json_tree"),

/*---- backward compatible format of explain response -----*/
SIMPLE("simple"),
Expand Down Expand Up @@ -52,6 +54,7 @@ public enum Format {
builder = new ImmutableMap.Builder<>();
builder.put(JSON.formatName, JSON);
builder.put(YAML.formatName, YAML);
builder.put(JSON_TREE.formatName, JSON_TREE);
builder.put(SIMPLE.formatName, SIMPLE);
builder.put(STANDARD.formatName, STANDARD);
builder.put(EXTENDED.formatName, EXTENDED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ public class ExplainPlanTest {

@Test
public void execute() {
doNothing().when(queryPlan).explain(any(), any());
doNothing().when(queryPlan).explain(any(), any(), any());

ExplainPlan explainPlan = new ExplainPlan(queryId, queryType, queryPlan, mode, explainListener);
explainPlan.execute();

verify(queryPlan, times(1)).explain(explainListener, mode);
verify(queryPlan, times(1)).explain(explainListener, mode, null);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ public void execute_no_page_size() {
@Test
public void explain_no_page_size() {
QueryPlan query = new QueryPlan(queryId, queryType, plan, queryService, queryListener);
query.explain(explainListener, mode);
query.explain(explainListener, mode, null);

verify(queryService, times(1)).explain(plan, queryType, null, explainListener, mode);
verify(queryService, times(1)).explain(plan, queryType, null, explainListener, mode, null);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import java.io.IOException;
import java.util.Locale;
import org.apache.commons.text.StringEscapeUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
Expand Down Expand Up @@ -3010,4 +3012,29 @@ public void testExplainUnion() throws IOException {
String expected = loadExpectedPlan("explain_union.yaml");
assertYamlEqualsIgnoreId(expected, actual);
}

@Test
public void testExplainJsonTreeFormat() throws IOException {
String query = "source=opensearch-sql_test_index_account | where age > 30 | fields age";
String result = explainQuery(query, Format.JSON_TREE, ExplainMode.STANDARD);

// Parse JSON response
JSONObject json = new JSONObject(result);
JSONObject calcite = json.getJSONObject("calcite");

// Verify logical plan is a structured JSON object (not a plain string)
JSONObject logical = calcite.getJSONObject("logical");
Assert.assertTrue("Logical plan should contain 'rels' array", logical.has("rels"));
JSONArray rels = logical.getJSONArray("rels");
Assert.assertTrue("Rels array should not be empty", rels.length() > 0);

// Verify first rel is a proper RelNode structure
JSONObject firstRel = rels.getJSONObject(0);
Assert.assertTrue("RelNode should have 'relOp' field", firstRel.has("relOp"));
Assert.assertTrue("RelNode should have 'id' field", firstRel.has("id"));

// Verify physical plan also has structured format
JSONObject physical = calcite.getJSONObject("physical");
Assert.assertTrue("Physical plan should contain 'rels' array", physical.has("rels"));
}
}
Loading
Loading