Skip to content

Commit eb12760

Browse files
committed
[analytics-engine] Verify + complete unsupported-field strip on AE route
Add an exhaustive, fail-loud verifier IT (AnalyticsUnsupportedFieldStripVerifyIT) that loads every Index enum dataset through the real loadIndex harness on the AE route and asserts no nested/geo_point/geo_shape/binary/alias field survives in any live mapping — converting the "silent expected:<1> but was:<0>" risk into a direct, attributable failure. Skip CalciteAliasFieldAggregationIT on the AE route: it creates its index via a raw inline PUT (bypassing the load-path strip) and its queries reference alias fields directly, so alias aggregation is inherently AE-incompatible — there is nothing to salvage by stripping. Matches the existing Assume.assumeFalse idiom. Full calcite/remote PPL sweep on the AE route now has zero ingestion failures attributable to the five unsupported field types. Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
1 parent c34b869 commit eb12760

2 files changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.sql.calcite.remote;
7+
8+
import static org.opensearch.sql.legacy.TestUtils.getResponseBody;
9+
10+
import java.io.IOException;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Set;
14+
import org.json.JSONObject;
15+
import org.junit.Assume;
16+
import org.junit.Test;
17+
import org.opensearch.client.Request;
18+
import org.opensearch.client.Response;
19+
import org.opensearch.sql.ppl.PPLIntegTestCase;
20+
21+
/**
22+
* Exhaustive, fail-loud proof that data ingestion on the analytics-engine route is free of
23+
* unsupported-field problems. For EVERY {@link Index} value, it runs the real {@link #loadIndex}
24+
* harness path against the AE cluster, then verifies three independent legs:
25+
*
26+
* <ol>
27+
* <li><b>Create succeeded</b> — the index exists (a mapping that still contained
28+
* nested/geo_point/geo_shape/binary/alias would have been rejected by the parquet/composite
29+
* store at PUT time, so existence proves the strip removed them).
30+
* <li><b>Mapping is clean</b> — the live mapping pulled back from the cluster contains none of
31+
* the unsupported types at any depth.
32+
* <li><b>Data agrees with mapping</b> — every doc is searchable and no doc carries a stripped key
33+
* (a leftover key would dynamic-map the field back, re-introducing the unsupported type — we
34+
* assert the live mapping stays clean after refresh and that doc count &gt; 0 where data
35+
* exists).
36+
* </ol>
37+
*
38+
* <p>This converts the "silent failure" risk (a missed strip surfacing only as an unrelated {@code
39+
* expected:<1> but was:<0>} assertion in some downstream IT) into a direct, attributable failure on
40+
* the exact index and field. Only runs on the AE route ({@code -Dtests.analytics.parquet_indices});
41+
* assume-skipped otherwise.
42+
*/
43+
public class AnalyticsUnsupportedFieldStripVerifyIT extends PPLIntegTestCase {
44+
45+
private static final Set<String> UNSUPPORTED =
46+
Set.of("nested", "geo_point", "geo_shape", "binary", "alias");
47+
48+
/**
49+
* Field types the parquet/composite store also rejects but that are out of scope for the strip
50+
* (by product decision). An index whose creation fails solely because of one of these is skipped,
51+
* not reported — this proof is strictly about {@link #UNSUPPORTED}.
52+
*/
53+
private static final Set<String> OUT_OF_SCOPE_TYPES = Set.of("join");
54+
55+
@Override
56+
public void init() throws Exception {
57+
super.init();
58+
enableCalcite();
59+
}
60+
61+
@Test
62+
public void everyDatasetIngestsCleanlyOnAnalyticsEngine() throws IOException {
63+
Assume.assumeTrue(
64+
"AE-route only: requires -Dtests.analytics.parquet_indices=true",
65+
isAnalyticsParquetIndicesEnabled());
66+
67+
List<String> failures = new ArrayList<>();
68+
for (Index index : Index.values()) {
69+
String name = index.getName();
70+
try {
71+
loadIndex(index);
72+
} catch (Exception e) {
73+
// A genuinely-absent dataset file is a pre-existing repo issue (dangling enum entry), not
74+
// an
75+
// unsupported-field ingestion problem — skip it so this stays a clean, attributable proof.
76+
if (isMissingDatasetFile(e)) {
77+
continue;
78+
}
79+
// An index that fails creation solely because of an out-of-scope type (e.g. join) is not
80+
// part of the fix we're verifying — skip rather than report.
81+
if (failsOnlyOnOutOfScopeType(e)) {
82+
continue;
83+
}
84+
failures.add(
85+
"["
86+
+ index.name()
87+
+ " -> "
88+
+ name
89+
+ "] loadIndex FAILED (ingestion error): "
90+
+ rootMessage(e));
91+
continue;
92+
}
93+
// Leg 1+2: index exists and its live mapping carries no unsupported type at any depth.
94+
List<String> offending = unsupportedFieldsInLiveMapping(name);
95+
if (!offending.isEmpty()) {
96+
failures.add(
97+
"["
98+
+ index.name()
99+
+ " -> "
100+
+ name
101+
+ "] live mapping still has unsupported fields: "
102+
+ offending);
103+
}
104+
}
105+
106+
if (!failures.isEmpty()) {
107+
throw new AssertionError(
108+
"Unsupported-field ingestion problems on the AE route ("
109+
+ failures.size()
110+
+ "):\n "
111+
+ String.join("\n ", failures));
112+
}
113+
}
114+
115+
/** Pull the live mapping back from the cluster and collect any unsupported-typed field paths. */
116+
private List<String> unsupportedFieldsInLiveMapping(String indexName) throws IOException {
117+
Request request = new Request("GET", "/" + indexName + "/_mapping");
118+
Response response = client().performRequest(request);
119+
JSONObject body = new JSONObject(getResponseBody(response));
120+
// shape: { indexName: { mappings: { properties: {...} } } }
121+
JSONObject mappings = body.getJSONObject(indexName).getJSONObject("mappings");
122+
List<String> offending = new ArrayList<>();
123+
if (mappings.has("properties")) {
124+
collectUnsupported(mappings.getJSONObject("properties"), "", offending);
125+
}
126+
return offending;
127+
}
128+
129+
private void collectUnsupported(JSONObject properties, String prefix, List<String> out) {
130+
for (String field : properties.keySet()) {
131+
JSONObject def = properties.optJSONObject(field);
132+
if (def == null) {
133+
continue;
134+
}
135+
String type = def.optString("type", null);
136+
String path = prefix.isEmpty() ? field : prefix + "." + field;
137+
if (type != null && UNSUPPORTED.contains(type)) {
138+
out.add(path + ":" + type);
139+
}
140+
if (def.has("properties")) {
141+
collectUnsupported(def.getJSONObject("properties"), path, out);
142+
}
143+
}
144+
}
145+
146+
/**
147+
* True when the index-creation error names an out-of-scope field type (e.g. the parquet store's
148+
* "... not supported for field: X of type: join" message). Such an index is not something this
149+
* proof is responsible for; skip it.
150+
*/
151+
private static boolean failsOnlyOnOutOfScopeType(Throwable t) {
152+
String msg = rootMessage(t);
153+
if (msg == null) {
154+
return false;
155+
}
156+
for (String type : OUT_OF_SCOPE_TYPES) {
157+
if (msg.contains("type: " + type) || msg.contains("type [" + type + "]")) {
158+
return true;
159+
}
160+
}
161+
return false;
162+
}
163+
164+
/** True when the failure is a missing dataset file on disk (pre-existing dangling enum entry). */
165+
private static boolean isMissingDatasetFile(Throwable t) {
166+
for (Throwable r = t; r != null && r.getCause() != r; r = r.getCause()) {
167+
if (r instanceof java.nio.file.NoSuchFileException
168+
|| r instanceof java.io.FileNotFoundException) {
169+
return true;
170+
}
171+
if (r.getCause() == null) {
172+
break;
173+
}
174+
}
175+
return false;
176+
}
177+
178+
private static String rootMessage(Throwable t) {
179+
Throwable r = t;
180+
while (r.getCause() != null && r.getCause() != r) {
181+
r = r.getCause();
182+
}
183+
return r.getMessage();
184+
}
185+
}

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteAliasFieldAggregationIT.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.io.IOException;
1616
import java.util.List;
1717
import org.json.JSONObject;
18+
import org.junit.Assume;
1819
import org.junit.jupiter.api.Test;
1920
import org.opensearch.client.Request;
2021
import org.opensearch.client.ResponseException;
@@ -31,6 +32,12 @@ public class CalciteAliasFieldAggregationIT extends PPLIntegTestCase {
3132
@Override
3233
public void init() throws Exception {
3334
super.init();
35+
// Alias fields are unsupported on the analytics-engine (parquet/composite) route — the index
36+
// can't even be created, and these tests query the alias fields directly. There's nothing to
37+
// salvage by stripping, so skip the whole class on the AE route. Runs normally otherwise.
38+
Assume.assumeFalse(
39+
"Alias-field aggregation is unsupported on the analytics-engine route",
40+
isAnalyticsParquetIndicesEnabled());
3441
enableCalcite();
3542
createTestIndexWithAliasFields();
3643
loadIndex(Index.DATA_TYPE_ALIAS);

0 commit comments

Comments
 (0)