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 @@ -45,6 +45,32 @@ public class CalcitePlanContext {
private static final ThreadLocal<Boolean> legacyPreferredFlag =
ThreadLocal.withInitial(() -> true);

/**
* Thread-local extra search-source JSON from the PPL request body. Set by PPLService before query
* execution and read by AbstractCalciteIndexScan when building the OpenSearch request. The JSON
* string is parsed via {@code SearchSourceBuilder.fromXContent()} and selectively merged into the
* index scan request.
*/
private static final ThreadLocal<String> extraSearchSource = new ThreadLocal<>();

public static void setExtraSearchSource(String json) {
extraSearchSource.set(json);
}

public static String getExtraSearchSource() {
return extraSearchSource.get();
}

public static void clearExtraSearchSource() {
extraSearchSource.remove();
}

/** Convenience check: does the extra search source contain a highlight clause? */
public static boolean hasHighlightInExtraSearchSource() {
String extra = extraSearchSource.get();
return extra != null && extra.contains("\"highlight\"");
}

@Getter @Setter private boolean isResolvingJoinCondition = false;
@Getter @Setter private boolean isResolvingSubquery = false;
@Getter @Setter private boolean inCoalesceFunction = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import static org.opensearch.sql.calcite.utils.PlanUtils.getRelation;
import static org.opensearch.sql.calcite.utils.PlanUtils.getRexCall;
import static org.opensearch.sql.calcite.utils.PlanUtils.transformPlanToAttachChild;
import static org.opensearch.sql.expression.HighlightExpression.HIGHLIGHT_FIELD;
import static org.opensearch.sql.utils.SystemIndexUtils.DATASOURCES_TABLE_NAME;

import com.google.common.base.Strings;
Expand Down Expand Up @@ -407,6 +408,15 @@ public RelNode visitProject(Project node, CalcitePlanContext context) {
if (!context.isResolvingSubquery()) {
context.setProjectVisited(true);
}
// When highlight is active, include _highlight in the projection so it survives
// through the Calcite pipeline. This matches DSL behavior where _source filtering
// does not affect highlights.
if (CalcitePlanContext.hasHighlightInExtraSearchSource()) {
int hlIndex = currentFields.indexOf(HIGHLIGHT_FIELD);
if (hlIndex >= 0) {
expandedFields.add(context.relBuilder.field(hlIndex));
}
Comment on lines +411 to +418
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's see the full context of the visitProject method and the code around lines 411-418
cd core/src/main/java/org/opensearch/sql/calcite && wc -l CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 97


🏁 Script executed:

# Read the visitProject method context (expanded range to understand the logic)
cd core/src/main/java/org/opensearch/sql/calcite && sed -n '380,430p' CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 1792


🏁 Script executed:

# Search for how _highlight is used downstream (extraction/handling)
rg "HIGHLIGHT_FIELD|_highlight|getHighlight" -n -A 3 -B 1 --type java

Repository: opensearch-project/sql

Length of output: 50378


🏁 Script executed:

# Check if there are similar guards against duplicate projection in the codebase
rg "expandedFields|alreadyProjected|anyMatch.*RexInputRef" -n --type java

Repository: opensearch-project/sql

Length of output: 1744


🏁 Script executed:

# Search for tests related to highlight projection
rg "highlight.*[Pp]roject|visitProject.*highlight" -n --type java

Repository: opensearch-project/sql

Length of output: 365


🏁 Script executed:

# Check if _highlight can be explicitly selected in user queries
# Look for test cases with explicit _highlight selection
rg "SELECT.*_highlight|project.*_highlight" -n --type java -B 2 -A 2 | head -50

Repository: opensearch-project/sql

Length of output: 1538


🏁 Script executed:

# Check expandProjectFields to see if it could include _highlight from user input
cd core/src/main/java/org/opensearch/sql/calcite && sed -n '440,495p' CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 2319


🏁 Script executed:

# Search for how currentFields is populated and whether _highlight would be present
cd core/src/main/java/org/opensearch/sql/calcite && sed -n '393,425p' CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 1328


🏁 Script executed:

# Check if there are tests specifically covering _highlight in projections with other fields
rg "highlight.*project|project.*highlight" -n --type java -A 5 | head -60

Repository: opensearch-project/sql

Length of output: 3601


🏁 Script executed:

# Check if _highlight is classified as a metadata field
rg "isMetadataField|METADATA|_highlight" -n --type java -B 2 -A 2 | grep -A 5 "isMetadataField"

Repository: opensearch-project/sql

Length of output: 4271


🏁 Script executed:

# Get the isMetadataField implementation
cd core/src/main/java/org/opensearch/sql/calcite && rg "isMetadataField" -A 5 CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 1952


🏁 Script executed:

# Check if _highlight is in METADATAFIELD_TYPE_MAP
rg "METADATAFIELD_TYPE_MAP|_highlight" -n --type java | grep -B 5 -A 5 "METADATAFIELD_TYPE_MAP"

Repository: opensearch-project/sql

Length of output: 7296


🏁 Script executed:

# Find where METADATAFIELD_TYPE_MAP is defined
fd "OpenSearchConstants.java" --type f | head -1 | xargs cat | head -100

Repository: opensearch-project/sql

Length of output: 1137


🏁 Script executed:

# Verify that _highlight is NOT in the metadata field map and would be included in wildcard projections
cd core/src/main/java/org/opensearch/sql/opensearch/storage && cat -n OpenSearchIndex.java | sed -n '65,75p'

Repository: opensearch-project/sql

Length of output: 172


🏁 Script executed:

# Check if there are any existing tests that combine SELECT * with highlight
rg "SELECT \*|AllFields" -n --type java integ-test/src/test/java/org/opensearch/sql/sql/HighlightFunctionIT.java

Repository: opensearch-project/sql

Length of output: 48


🏁 Script executed:

# Find the correct path for OpenSearchIndex.java
fd "OpenSearchIndex.java" --type f

Repository: opensearch-project/sql

Length of output: 243


🏁 Script executed:

# Check the METADATAFIELD_TYPE_MAP definition to confirm _highlight is NOT in it
rg "METADATAFIELD_TYPE_MAP\|_highlight" opensearch/src/main/java/org/opensearch/sql/opensearch/storage/OpenSearchIndex.java -A 10 -B 2

Repository: opensearch-project/sql

Length of output: 48


🏁 Script executed:

# Check if tests verify SELECT * with highlight scenarios
rg "SELECT.*\*.*highlight|highlight.*SELECT" integ-test/ -n --type java -A 3 -B 1

Repository: opensearch-project/sql

Length of output: 48


🏁 Script executed:

# Look for expandWildcardPattern to understand if _highlight gets included
rg "expandWildcardPattern|WildcardUtils" -n --type java -A 5 | head -50

Repository: opensearch-project/sql

Length of output: 5426


🏁 Script executed:

# Verify the exact scenario: when ALL fields are selected, does _highlight get added to expandedFields?
# Trace through: AllFields case adds all non-metadata fields
# _highlight is in currentFields (added by CalciteLogicalIndexScan when highlight config is set)
# _highlight is NOT in METADATAFIELD_TYPE_MAP, so !isMetadataField returns true
# So _highlight WOULD be added to expandedFields via the AllFields expansion
# Then the highlight logic adds it AGAIN at line 417

# Confirm _highlight is added to schema when highlight is active
rg "buildSchema|HIGHLIGHT_FIELD" opensearch/src/main/java/org/opensearch/sql/opensearch/storage/scan/CalciteLogicalIndexScan.java -B 3 -A 3

Repository: opensearch-project/sql

Length of output: 1710


🏁 Script executed:

# Check if there are integration tests that cover SELECT * with highlight
fd "HighlightFunctionIT.java" --type f -exec cat {} \; | head -150

Repository: opensearch-project/sql

Length of output: 253


🏁 Script executed:

# Read the HighlightFunctionIT.java test file
cat integ-test/src/test/java/org/opensearch/sql/sql/HighlightFunctionIT.java | head -200

Repository: opensearch-project/sql

Length of output: 7756


🏁 Script executed:

# Let me verify the exact duplicate scenario one more time by checking both code paths:
# Path 1: AllFields expansion (lines 479-482)
# Path 2: Explicit highlight logic (lines 414-417)

# Confirm that expandProjectFields is called BEFORE the highlight logic
cd core/src/main/java/org/opensearch/sql/calcite && sed -n '393,425p' CalciteRelNodeVisitor.java | cat -n

Repository: opensearch-project/sql

Length of output: 1559


🏁 Script executed:

# Now trace through a hypothetical SELECT * query with highlight:
# 1. Project node has AllFields as projectList
# 2. visitProject is called
# 3. isSingleAllFieldsProject returns true
# 4. handleAllFieldsProject is called (not visitProject logic at 411-420)

# Check handleAllFieldsProject
cd core/src/main/java/org/opensearch/sql/calcite && sed -n '424,445p' CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 951


🏁 Script executed:

# Check what happens in the non-AllFields-only case
# If someone does: SELECT name, * with highlight
# Then isSingleAllFieldsProject returns FALSE
# Then expandProjectFields is called
# Then highlight logic runs

# Verify: when is expandProjectFields called?
cd core/src/main/java/org/opensearch/sql/calcite && sed -n '395,404p' CalciteRelNodeVisitor.java

Repository: opensearch-project/sql

Length of output: 397


🏁 Script executed:

# Final verification: construct the exact scenario that would trigger the bug
# SELECT name, * with highlight enabled
# Trace: 
# 1. Project has [Field("name"), AllFields()]
# 2. isSingleAllFieldsProject returns FALSE (size=2, not 1)
# 3. expandProjectFields processes both:
#    - Field "name" → added to expandedFields
#    - AllFields → expands to all currentFields (which includes _highlight)
# 4. Then highlight logic adds _highlight AGAIN

# Check if there are tests covering SELECT <field>, * scenario
rg "SELECT.*,.*\*" integ-test/ core/src/test -n --type java | head -20

Repository: opensearch-project/sql

Length of output: 3138


Guard against duplicate _highlight projection when using SELECT with mixed explicit fields and wildcards.

When highlight is enabled and a user queries with mixed explicit fields and AllFields (e.g., SELECT name, *), the code can add _highlight twice to the projection:

  1. expandProjectFields includes _highlight via AllFields expansion (since _highlight is not in METADATAFIELD_TYPE_MAP)
  2. The highlight preservation logic at lines 414-417 adds it again unconditionally

Calcite may rename duplicate columns (e.g., _highlight0, _highlight1), which breaks downstream extraction that uses exact name matching via tuple.get(HIGHLIGHT_FIELD). The suggested guard checking for existing RexInputRef matches prevents this duplication.

🔧 Suggested guard
       if (CalcitePlanContext.getHighlightConfig() != null) {
         int hlIndex = currentFields.indexOf(HIGHLIGHT_FIELD);
-        if (hlIndex >= 0) {
-          expandedFields.add(context.relBuilder.field(hlIndex));
+        if (hlIndex >= 0) {
+          boolean alreadyProjected =
+              expandedFields.stream()
+                  .anyMatch(
+                      rex ->
+                          (rex instanceof RexInputRef ref && ref.getIndex() == hlIndex)
+                              || (rex instanceof RexCall call
+                                  && call.getKind() == SqlKind.AS
+                                  && call.getOperands().get(0) instanceof RexInputRef ref
+                                  && ref.getIndex() == hlIndex));
+          if (!alreadyProjected) {
+            expandedFields.add(context.relBuilder.field(hlIndex));
+          }
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// When highlight is active, include _highlight in the projection so it survives
// through the Calcite pipeline. This matches DSL behavior where _source filtering
// does not affect highlights.
if (CalcitePlanContext.getHighlightConfig() != null) {
int hlIndex = currentFields.indexOf(HIGHLIGHT_FIELD);
if (hlIndex >= 0) {
expandedFields.add(context.relBuilder.field(hlIndex));
}
// When highlight is active, include _highlight in the projection so it survives
// through the Calcite pipeline. This matches DSL behavior where _source filtering
// does not affect highlights.
if (CalcitePlanContext.getHighlightConfig() != null) {
int hlIndex = currentFields.indexOf(HIGHLIGHT_FIELD);
if (hlIndex >= 0) {
boolean alreadyProjected =
expandedFields.stream()
.anyMatch(
rex ->
(rex instanceof RexInputRef ref && ref.getIndex() == hlIndex)
|| (rex instanceof RexCall call
&& call.getKind() == SqlKind.AS
&& call.getOperands().get(0) instanceof RexInputRef ref
&& ref.getIndex() == hlIndex));
if (!alreadyProjected) {
expandedFields.add(context.relBuilder.field(hlIndex));
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java`
around lines 411 - 418, When highlight is enabled
(CalcitePlanContext.getHighlightConfig() != null) the highlight preservation
block in CalciteRelNodeVisitor can add HIGHLIGHT_FIELD twice into expandedFields
(duplicate of expandProjectFields' AllFields expansion); modify the logic around
currentFields/index hlIndex and expandedFields to first scan expandedFields for
an existing RexInputRef that references the same input index (or any existing
projection that already corresponds to HIGHLIGHT_FIELD) and only add
context.relBuilder.field(hlIndex) if no matching RexInputRef exists, so the
projection is not duplicated and downstream tuple.get(HIGHLIGHT_FIELD) continues
to work.

}
context.relBuilder.project(expandedFields);
}
return context.relBuilder.peek();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.opensearch.sql.ast.statement.ExplainMode;
import org.opensearch.sql.common.response.ResponseListener;
import org.opensearch.sql.executor.ExecutionEngine;
Expand All @@ -22,6 +23,15 @@ public abstract class AbstractPlan {

@Getter protected final QueryType queryType;

/**
* Extra search-source JSON from the PPL request body. Set by PPLService before submitting the
* plan to the query manager. The plan carries this across the thread boundary (REST handler
* thread → sql-worker thread), and the worker thread sets it as a ThreadLocal before Calcite
* planning and execution begin. The JSON is later parsed via {@code
* SearchSourceBuilder.fromXContent()} and selectively merged into the index scan request.
*/
@Getter @Setter private String extraSearchSource;

/** Start query execution. */
public abstract void execute();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.opensearch.sql.executor.execution;

import org.opensearch.sql.ast.statement.ExplainMode;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.common.response.ResponseListener;
import org.opensearch.sql.executor.ExecutionEngine;
import org.opensearch.sql.executor.QueryId;
Expand Down Expand Up @@ -34,7 +35,19 @@ public ExplainPlan(

@Override
public void execute() {
plan.explain(explainListener, mode);
setExtraSearchSourceThreadLocal();
try {
plan.explain(explainListener, mode);
} finally {
CalcitePlanContext.clearExtraSearchSource();
}
}

private void setExtraSearchSourceThreadLocal() {
String extra = getExtraSearchSource();
if (extra != null) {
CalcitePlanContext.setExtraSearchSource(extra);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.opensearch.sql.ast.statement.ExplainMode;
import org.opensearch.sql.ast.tree.Paginate;
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.common.response.ResponseListener;
import org.opensearch.sql.executor.ExecutionEngine;
import org.opensearch.sql.executor.QueryId;
Expand Down Expand Up @@ -60,10 +61,15 @@ public QueryPlan(

@Override
public void execute() {
if (pageSize.isPresent()) {
queryService.execute(new Paginate(pageSize.get(), plan), getQueryType(), listener);
} else {
queryService.execute(plan, getQueryType(), listener);
setExtraSearchSourceThreadLocal();
try {
if (pageSize.isPresent()) {
queryService.execute(new Paginate(pageSize.get(), plan), getQueryType(), listener);
} else {
queryService.execute(plan, getQueryType(), listener);
}
} finally {
CalcitePlanContext.clearExtraSearchSource();
}
}

Expand All @@ -78,4 +84,11 @@ public void explain(
queryService.explain(plan, getQueryType(), listener, mode);
}
}

private void setExtraSearchSourceThreadLocal() {
String extra = getExtraSearchSource();
if (extra != null) {
CalcitePlanContext.setExtraSearchSource(extra);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
/** Highlight Expression. */
@Getter
public class HighlightExpression extends FunctionExpression {
/** The field name used to store highlight data on ExprTupleValue rows. */
public static final String HIGHLIGHT_FIELD = "_highlight";

private final Expression highlightField;
private final ExprType type;

Expand All @@ -46,7 +49,7 @@ public HighlightExpression(Expression highlightField) {
*/
@Override
public ExprValue valueOf(Environment<Expression, ExprValue> valueEnv) {
String refName = "_highlight";
String refName = HIGHLIGHT_FIELD;
// Not a wilcard expression
if (this.type == ExprCoreType.ARRAY) {
refName += "." + StringUtils.unquoteText(getHighlightField().toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
package org.opensearch.sql.executor.execution;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.sql.ast.statement.ExplainMode;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.common.response.ResponseListener;
import org.opensearch.sql.executor.ExecutionEngine;
import org.opensearch.sql.executor.QueryId;
Expand Down Expand Up @@ -56,4 +60,36 @@ public void explainThrowException() {
});
assertEquals("explain query can not been explained.", unsupportedExplainException.getMessage());
}

@Test
public void execute_sets_extra_search_source_threadlocal_for_explain() {
String extraSearchSource = "{\"highlight\":{\"fields\":{\"*\":{}}}}";
AtomicReference<String> captured = new AtomicReference<>();

doAnswer(
invocation -> {
captured.set(CalcitePlanContext.getExtraSearchSource());
return null;
})
.when(queryPlan)
.explain(any(), any());

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

assertEquals(extraSearchSource, captured.get());
}

@Test
public void execute_clears_extra_search_source_threadlocal_after_explain() {
String extraSearchSource = "{\"highlight\":{\"fields\":{\"*\":{}}}}";
doNothing().when(queryPlan).explain(any(), any());

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

assertNull(CalcitePlanContext.getExtraSearchSource());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@

package org.opensearch.sql.executor.execution;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.lang3.NotImplementedException;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
Expand All @@ -22,6 +26,7 @@
import org.mockito.junit.jupiter.MockitoExtension;
import org.opensearch.sql.ast.statement.ExplainMode;
import org.opensearch.sql.ast.tree.UnresolvedPlan;
import org.opensearch.sql.calcite.CalcitePlanContext;
import org.opensearch.sql.common.response.ResponseListener;
import org.opensearch.sql.executor.DefaultExecutionEngine;
import org.opensearch.sql.executor.ExecutionEngine;
Expand Down Expand Up @@ -126,4 +131,35 @@ public void onFailure(Exception e) {
},
mode);
}

@Test
public void execute_sets_extra_search_source_threadlocal() {
String extraSearchSource = "{\"highlight\":{\"fields\":{\"*\":{}}}}";
AtomicReference<String> captured = new AtomicReference<>();

doAnswer(
invocation -> {
captured.set(CalcitePlanContext.getExtraSearchSource());
return null;
})
.when(queryService)
.execute(any(), any(), any());

QueryPlan query = new QueryPlan(queryId, queryType, plan, queryService, queryListener);
query.setExtraSearchSource(extraSearchSource);
query.execute();

assertEquals(extraSearchSource, captured.get());
}

@Test
public void execute_clears_extra_search_source_threadlocal_after_execution() {
String extraSearchSource = "{\"highlight\":{\"fields\":{\"*\":{}}}}";

QueryPlan query = new QueryPlan(queryId, queryType, plan, queryService, queryListener);
query.setExtraSearchSource(extraSearchSource);
query.execute();

assertNull(CalcitePlanContext.getExtraSearchSource());
}
}
Loading
Loading