Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
992a78c
Add non-fatal warning channel to PPL query response
ahkcs Jul 27, 2026
33aed50
Return a partial result instead of exhausting PIT on a mapping conflict
ahkcs Jul 27, 2026
c354738
Gate partial results on a warning-capable response format
ahkcs Jul 27, 2026
70c344c
Flatten per-index mappings when partitioning for partial results
ahkcs Jul 27, 2026
12867b2
Refine partial-result index selection and warning wording
ahkcs Jul 27, 2026
ff7845f
Truncate the excluded-index list in the partial-result warning
ahkcs Jul 27, 2026
7585190
Fix partial-result warning wording to reflect the pushdown criterion
ahkcs Jul 27, 2026
bd883d8
Extract partial-result partitioning into its own class with unit tests
ahkcs Jul 27, 2026
95d9ac6
Allow a per-request override for partial-result mode
ahkcs Jul 28, 2026
14a35cb
Simplify partial-result warning to name the excluded indices and the fix
ahkcs Jul 28, 2026
01a2436
Do not resolve response format for explain requests
ahkcs Jul 28, 2026
4b04df9
Cover the null-warnings branch in QueryResult to satisfy protocol cov…
ahkcs Jul 28, 2026
806a07a
Address review: rename the partial-result setting and fold the fallba…
ahkcs Jul 29, 2026
0d4c8c0
Reuse the already-fetched index mappings for partial-result partitioning
ahkcs Jul 29, 2026
6379c85
Fix formatting of the renamed partial-result setting key
ahkcs Jul 29, 2026
40dad6e
Revert the index-mapping reuse optimization: it exposed a merge-mutat…
ahkcs Jul 29, 2026
f5be912
Reuse the already-fetched index mappings, and stop the merge mutating…
ahkcs Jul 30, 2026
7631342
Clear the per-request partial-result state after each query
ahkcs Jul 30, 2026
0018315
Update explain golden files for the changed OpenSearchDataType serial…
ahkcs Jul 30, 2026
b860306
Decide partial-result mode before pushdown analysis, not after it fails
ahkcs Aug 4, 2026
43e5668
Document the partial-result-on-mapping-conflict setting
ahkcs Aug 5, 2026
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 @@ -48,6 +48,7 @@ public enum Key {

/** Query Settings. */
FIELD_TYPE_TOLERANCE("plugins.query.field_type_tolerance"),
PARTIAL_RESULT_ON_MAPPING_CONFLICT("plugins.query.partial_result.on_mapping_conflict.enabled"),

/** Common Settings for SQL and PPL. */
QUERY_MEMORY_LIMIT("plugins.query.memory_limit"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public class QueryContext {

private static final String PROFILE_KEY = "profile";

private static final String WARNINGS_SUPPORTED_KEY = "warnings_supported";

private static final String PARTIAL_RESULT_OVERRIDE_KEY = "partial_result_override";

/**
* Generates a random UUID and adds to the {@link ThreadContext} as the request id.
*
Expand Down Expand Up @@ -84,4 +88,48 @@ 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));
}

/**
* Record a per-request override for partial-result mode. When set, it takes precedence over the
* cluster setting: {@code true} forces partial mode on for this request, {@code false} forces it
* off. A {@code null} value (the default) leaves the decision to the cluster setting.
*
* @param override the per-request preference, or null to defer to the cluster setting
*/
public static void setPartialResultOverride(Boolean override) {
if (override == null) {
ThreadContext.remove(PARTIAL_RESULT_OVERRIDE_KEY);
} else {
ThreadContext.put(PARTIAL_RESULT_OVERRIDE_KEY, Boolean.toString(override));
}
}

/**
* @return the per-request partial-result override, or {@code null} when the request expressed no
* preference (in which case the cluster setting decides).
*/
public static Boolean getPartialResultOverride() {
String value = ThreadContext.get(PARTIAL_RESULT_OVERRIDE_KEY);
return value == null ? null : Boolean.parseBoolean(value);
}
}
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 @@ -63,6 +64,15 @@ public class CalcitePlanContext {
*/
public static final ThreadLocal<String> executionPool = 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 @@ -251,6 +261,27 @@ public static void clearTimewrapSignals() {
timewrapUnitName.set(null);
timewrapSeries.set(null);
executionPool.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;
}

/**
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;
}
47 changes: 47 additions & 0 deletions docs/user/admin/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,53 @@ Result set::
}
}

plugins.query.partial_result.on_mapping_conflict.enabled
========================================================

Version
-------
3.8

Description
-----------

Controls how an aggregation behaves when its group-by field is mapped inconsistently across the queried indices -- for example ``keyword`` in some indices of a wildcard pattern and ``text`` (without a ``.keyword`` sub-field) in others. Such a field collapses to ``text``-without-``.keyword`` across the pattern, which has no doc values, so the aggregation cannot be pushed down natively and instead runs as a per-document script over ``_source`` -- correct, but a full scan of every document.

When this setting is ``false`` (the default), that complete-but-slow result is returned. When set to ``true``, the aggregation is pushed down over only the subset of indices where the field is aggregatable, and the response carries a ``PARTIAL_RESULT`` warning naming the excluded indices and the remedy (map the field as ``keyword`` everywhere). The result is therefore **partial** -- documents in the excluded indices are not counted -- so the setting is off by default and only takes effect for response formats that can surface the warning (the JSON format; CSV/raw/visualization responses fall through to the complete result rather than silently dropping data).

The behavior can also be overridden per request with the ``partial_result`` boolean field in the query body, which takes precedence over this cluster setting. Here is an example enabling it at the cluster level::

>> curl -H 'Content-Type: application/json' -X PUT localhost:9200/_plugins/_query/settings -d '{
"transient" : {
"plugins.query.partial_result.on_mapping_conflict.enabled" : true
}
}'

Result set::

{
"acknowledged" : true,
"persistent" : { },
"transient" : {
"plugins" : {
"query" : {
"partial_result" : {
"on_mapping_conflict" : {
"enabled" : "true"
}
}
}
}
}
}

Per-request override example, opting a single query into a partial result regardless of the cluster setting::

>> curl -H 'Content-Type: application/json' -X POST localhost:9200/_plugins/_ppl -d '{
"query" : "source=logs-* | stats count() by service",
"partial_result" : true
}'

plugins.query.buckets
=====================

Expand Down
Loading
Loading