Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9c889de
Add non-fatal warning channel to PPL query response
ahkcs Jul 27, 2026
61155e6
Return a partial result instead of exhausting PIT on a mapping conflict
ahkcs Jul 27, 2026
ecfae9a
Gate partial results on a warning-capable response format
ahkcs Jul 27, 2026
b981725
Flatten per-index mappings when partitioning for partial results
ahkcs Jul 27, 2026
04bbef0
Refine partial-result index selection and warning wording
ahkcs Jul 27, 2026
7ae4a54
Truncate the excluded-index list in the partial-result warning
ahkcs Jul 27, 2026
c79ccd9
Fix partial-result warning wording to reflect the pushdown criterion
ahkcs Jul 27, 2026
73806e5
Extract partial-result partitioning into its own class with unit tests
ahkcs Jul 27, 2026
a82efa9
Allow a per-request override for partial-result mode
ahkcs Jul 28, 2026
a3c63f0
Simplify partial-result warning to name the excluded indices and the fix
ahkcs Jul 28, 2026
c11b191
Do not resolve response format for explain requests
ahkcs Jul 28, 2026
7144b52
Cover the null-warnings branch in QueryResult to satisfy protocol cov…
ahkcs Jul 28, 2026
58a188f
Address review: rename the partial-result setting and fold the fallba…
ahkcs Jul 29, 2026
d52d0ca
Reuse the already-fetched index mappings for partial-result partitioning
ahkcs Jul 29, 2026
7f953f9
Fix formatting of the renamed partial-result setting key
ahkcs Jul 29, 2026
afc4d33
Revert the index-mapping reuse optimization: it exposed a merge-mutat…
ahkcs Jul 29, 2026
7499a99
Reuse the already-fetched index mappings, and stop the merge mutating…
ahkcs Jul 30, 2026
e1a1744
Clear the per-request partial-result state after each query
ahkcs Jul 30, 2026
8a6d679
Update explain golden files for the changed OpenSearchDataType serial…
ahkcs Jul 30, 2026
10644c2
Decide partial-result mode before pushdown analysis, not after it fails
ahkcs Aug 4, 2026
5efc7f2
Document the partial-result-on-mapping-conflict setting
ahkcs Aug 5, 2026
b6f543a
Tighten inline comments on the partial-result path
ahkcs Aug 11, 2026
12bec05
Address review: mark setting experimental, consolidate enable check
ahkcs Aug 17, 2026
a3204d3
Drop the redundant post-failure partial-result fallbacks
ahkcs Aug 17, 2026
7db73c0
Tighten inline comments
ahkcs Aug 17, 2026
ad3c0a5
Bail on a non-text type conflict instead of excluding the index
ahkcs Aug 17, 2026
cbf5074
Partition partial results on the group key's source fields
ahkcs Aug 17, 2026
11b57b2
Add IT for a multi-field expression group key
ahkcs Aug 17, 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 @@ -8,6 +8,7 @@
import java.util.Map;
import java.util.UUID;
import org.apache.logging.log4j.ThreadContext;
import org.opensearch.sql.common.setting.Settings;

/**
* Utility class for recording and accessing context for the query being executed. Implementation
Expand All @@ -22,6 +23,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 +89,53 @@ 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));
}
}

/**
* Whether partial-result mode applies to the current query. The per-request override wins when
* present; otherwise the cluster setting decides.
*
* @param settings the plugin settings to read the cluster default from
*/
public static boolean isPartialResultEnabled(Settings settings) {
String override = ThreadContext.get(PARTIAL_RESULT_OVERRIDE_KEY);
if (override != null) {
return Boolean.parseBoolean(override);
}
return settings.getSettingValue(Settings.Key.PARTIAL_RESULT_ON_MAPPING_CONFLICT);
}
}
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 @@ -266,6 +276,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 [Experimental]
=======================================================================

Version
-------
Since 3.9

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

This setting is experimental; its name, values, and default may change in a future release. 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