Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public enum Key {
CALCITE_PUSHDOWN_ROWCOUNT_ESTIMATION_FACTOR(
"plugins.calcite.pushdown.rowcount.estimation.factor"),
CALCITE_SUPPORT_ALL_JOIN_TYPES("plugins.calcite.all_join_types.allowed"),
CALCITE_PARTIAL_RESULT_ON_MAPPING_CONFLICT(
"plugins.calcite.partial_result.on_mapping_conflict.enabled"),

/** Query Settings. */
FIELD_TYPE_TOLERANCE("plugins.query.field_type_tolerance"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class QueryContext {

private static final String PROFILE_KEY = "profile";

private static final String WARNINGS_SUPPORTED_KEY = "warnings_supported";

/**
* Generates a random UUID and adds to the {@link ThreadContext} as the request id.
*
Expand Down Expand Up @@ -84,4 +86,24 @@ public static void setProfile(boolean profileEnabled) {
public static boolean isProfileEnabled() {
return Boolean.parseBoolean(ThreadContext.get(PROFILE_KEY));
}

/**
* Record whether the requested response format can surface non-fatal warnings. Features that
* return a knowingly-partial result gate on this so they never silently drop data into a format
* (CSV/RAW) that has no warning channel.
*
* @param supported whether the response format carries a warnings channel
*/
public static void setWarningsSupported(boolean supported) {
ThreadContext.put(WARNINGS_SUPPORTED_KEY, Boolean.toString(supported));
}

/**
* @return true if the response format for the current request can surface warnings. Defaults to
* false when unset, so a caller that never declared support cannot get a silent partial
* result.
*/
public static boolean isWarningsSupported() {
return Boolean.parseBoolean(ThreadContext.get(WARNINGS_SUPPORTED_KEY));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.opensearch.sql.calcite.utils.CalciteToolsHelper.OpenSearchRelBuilder;
import org.opensearch.sql.common.setting.Settings;
import org.opensearch.sql.executor.QueryType;
import org.opensearch.sql.executor.Warning;
import org.opensearch.sql.expression.function.FunctionProperties;

public class CalcitePlanContext {
Expand Down Expand Up @@ -58,6 +59,15 @@ public class CalcitePlanContext {
/** Timewrap series mode: "relative", "short", or "exact". */
public static final ThreadLocal<String> timewrapSeries = new ThreadLocal<>();

/**
* Non-fatal warnings raised during planning (e.g. a partial result over a subset of indices) to
* be attached to the query response by the execution engine. Drained in {@code
* OpenSearchExecutionEngine.buildResultSet} and cleared with the other lifecycle signals so it
* never leaks onto the next query on a pooled worker thread.
*/
private static final ThreadLocal<List<Warning>> pendingWarnings =
ThreadLocal.withInitial(ArrayList::new);

/** Thread-local switch that tells whether the current query prefers legacy behavior. */
private static final ThreadLocal<Boolean> legacyPreferredFlag =
ThreadLocal.withInitial(() -> true);
Expand Down Expand Up @@ -245,6 +255,27 @@ public static void clearTimewrapSignals() {
stripNullColumns.set(false);
timewrapUnitName.set(null);
timewrapSeries.set(null);
pendingWarnings.remove();
}

/** Records a non-fatal warning to be attached to the response for the current query. */
public static void addWarning(Warning warning) {
pendingWarnings.get().add(warning);
}

/**
* Returns and clears the warnings collected for the current query, de-duplicated by value. The
* planner may fire a rule that raises a warning more than once for equivalent plan alternatives,
* so identical warnings are collapsed to one.
*/
public static List<Warning> drainWarnings() {
List<Warning> warnings = pendingWarnings.get();
if (warnings.isEmpty()) {
return List.of();
}
List<Warning> drained = warnings.stream().distinct().toList();
pendingWarnings.remove();
return drained;
}

public void pushForeachBindings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ class QueryResponse {
private final Cursor cursor;
@lombok.Setter private QueryProfile profile;
@lombok.Setter private Throwable error;

/** Non-fatal notices attached to a successful result; empty for a plain success. */
@lombok.Setter private List<Warning> warnings = List.of();
}

@Data
Expand Down
34 changes: 34 additions & 0 deletions core/src/main/java/org/opensearch/sql/executor/Warning.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.sql.executor;

import lombok.Data;

/**
* A non-fatal notice attached to an otherwise-successful query response. Carried through the
* response path so consumers can distinguish a correct-but-noteworthy result (e.g. a partial result
* over a subset of indices) from a plain success, without turning it into an error.
*/
@Data
public class Warning {

/**
* The result is complete for the indices it covers but omits one or more indices that could not
* be served (e.g. a mapping conflict that prevents aggregation pushdown). This is a cross-surface
* contract: consumers such as OpenSearch Dashboards branch on this {@code type} value, so it must
* not change without coordinating those consumers.
*/
public static final String TYPE_PARTIAL_RESULT = "PARTIAL_RESULT";

/** Machine-readable category, e.g. {@link #TYPE_PARTIAL_RESULT}. */
private final String type;

/** Short human-readable summary. */
private final String message;

/** Optional longer explanation with the specifics and remedy; may be null. */
private final String detail;
}
Loading
Loading