Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
83fd850
Add non-fatal warning channel to PPL query response
ahkcs Jul 27, 2026
f8330d5
Return a partial result instead of exhausting PIT on a mapping conflict
ahkcs Jul 27, 2026
a6085e6
Gate partial results on a warning-capable response format
ahkcs Jul 27, 2026
834b156
Flatten per-index mappings when partitioning for partial results
ahkcs Jul 27, 2026
de789c1
Refine partial-result index selection and warning wording
ahkcs Jul 27, 2026
1262d8e
Truncate the excluded-index list in the partial-result warning
ahkcs Jul 27, 2026
66367c0
Fix partial-result warning wording to reflect the pushdown criterion
ahkcs Jul 27, 2026
208589d
Extract partial-result partitioning into its own class with unit tests
ahkcs Jul 27, 2026
a3fa7b9
Allow a per-request override for partial-result mode
ahkcs Jul 28, 2026
960984b
Simplify partial-result warning to name the excluded indices and the fix
ahkcs Jul 28, 2026
a5759ff
Do not resolve response format for explain requests
ahkcs Jul 28, 2026
090dd39
Cover the null-warnings branch in QueryResult to satisfy protocol cov…
ahkcs Jul 28, 2026
60d3a53
Address review: rename the partial-result setting and fold the fallba…
ahkcs Jul 29, 2026
f50c008
Reuse the already-fetched index mappings for partial-result partitioning
ahkcs Jul 29, 2026
4216040
Fix formatting of the renamed partial-result setting key
ahkcs Jul 29, 2026
11ae75e
Revert the index-mapping reuse optimization: it exposed a merge-mutat…
ahkcs Jul 29, 2026
3da0b58
Reuse the already-fetched index mappings, and stop the merge mutating…
ahkcs Jul 30, 2026
05a78d0
Clear the per-request partial-result state after each query
ahkcs Jul 30, 2026
36e3474
Update explain golden files for the changed OpenSearchDataType serial…
ahkcs Jul 30, 2026
b30124b
Decide partial-result mode before pushdown analysis, not after it fails
ahkcs Aug 4, 2026
091eebd
Document the partial-result-on-mapping-conflict setting
ahkcs Aug 5, 2026
6037584
Tighten inline comments on the partial-result path
ahkcs Aug 11, 2026
5220471
Address review: mark setting experimental, consolidate enable check
ahkcs Aug 17, 2026
8a722ac
Drop the redundant post-failure partial-result fallbacks
ahkcs Aug 17, 2026
2a9058a
Tighten inline comments
ahkcs Aug 17, 2026
1904867
Bail on a non-text type conflict instead of excluding the index
ahkcs Aug 17, 2026
fc803bd
Partition partial results on the group key's source fields
ahkcs Aug 17, 2026
0a9020c
Add IT for a multi-field expression group key
ahkcs Aug 17, 2026
b89193f
Handle a non-text type vs text conflict in partial results
ahkcs Aug 20, 2026
009cd0b
Revert non-text type generalization; scope to text/keyword collapse
ahkcs Aug 20, 2026
97aa640
Generalize partial results to any non-aggregatable-vs-aggregatable co…
ahkcs Aug 20, 2026
d647281
Address review: drop the special keyword token; sort only when buildi…
ahkcs Sep 1, 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 @@ -250,6 +260,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 @@ -287,6 +287,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