Skip to content

Commit 13d1081

Browse files
committed
[analytics-engine] Keep binary in scan set; dedup strip type-list; preserve untouched bulk docs
binary maps to VARBINARY in the analytics-engine schema builder (same as ip) and is a registered parquet field type, so it scans fine - drop it from the unsupported-field strip set, which is now {nested, geo_point, geo_shape, alias}. Stripping binary needlessly mutated fixtures and risked masking a real binary-handling bug instead of letting the assertion surface it. Make UNSUPPORTED_FIELD_TYPES the single source of truth (public) and have AnalyticsUnsupportedFieldStripVerifyIT import it instead of re-listing, so the stripper and the verifier cannot drift. Add a byte-identity guard to stripBulkFields: a bulk doc line is only re-serialized when it actually carried a dropped key, so untouched docs pass through verbatim. Signed-off-by: Eric Wei <mengwei.eric@gmail.com>
1 parent eb12760 commit 13d1081

3 files changed

Lines changed: 77 additions & 19 deletions

File tree

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*
2626
* <ol>
2727
* <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
28+
* nested/geo_point/geo_shape/alias would have been rejected by the parquet/composite
2929
* store at PUT time, so existence proves the strip removed them).
3030
* <li><b>Mapping is clean</b> — the live mapping pulled back from the cluster contains none of
3131
* the unsupported types at any depth.
@@ -42,8 +42,10 @@
4242
*/
4343
public class AnalyticsUnsupportedFieldStripVerifyIT extends PPLIntegTestCase {
4444

45+
// Single source of truth — the same set the load-path strip uses (TestUtils.AnalyticsIndexConfig).
46+
// Importing it (rather than re-listing) guarantees the verifier and the stripper can't drift.
4547
private static final Set<String> UNSUPPORTED =
46-
Set.of("nested", "geo_point", "geo_shape", "binary", "alias");
48+
org.opensearch.sql.legacy.TestUtils.AnalyticsIndexConfig.UNSUPPORTED_FIELD_TYPES;
4749

4850
/**
4951
* Field types the parquet/composite store also rejects but that are out of scope for the strip

integ-test/src/test/java/org/opensearch/sql/legacy/AnalyticsFieldStripTests.java

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
/**
1919
* Pure-logic coverage for the analytics-engine field strip (no filesystem / cluster). Verifies that
20-
* nested/geo_point/geo_shape/binary/alias fields are removed from mappings and bulk data on the AE
21-
* route, and that everything is a no-op when the route is off.
20+
* nested/geo_point/geo_shape/alias fields are removed from mappings and bulk data on the AE route,
21+
* that a supported {@code binary} field is left intact, and that everything is a no-op when the
22+
* route is off.
2223
*/
2324
public class AnalyticsFieldStripTests {
2425

@@ -37,6 +38,7 @@ private void enable() {
3738
+ "\"nested_value\":{\"type\":\"nested\"},"
3839
+ "\"geo_point_value\":{\"type\":\"geo_point\"},"
3940
+ "\"geo_shape_value\":{\"type\":\"geo_shape\"},"
41+
// binary is a SUPPORTED scan type (binary -> VARBINARY); it must be kept, not stripped.
4042
+ "\"binary_value\":{\"type\":\"binary\"},"
4143
+ "\"alias_value\":{\"type\":\"alias\",\"path\":\"keep_text\"},"
4244
+ "\"obj\":{\"properties\":{"
@@ -51,15 +53,14 @@ public void mappingStrip_removesUnsupportedRecursively_andReportsTopLevel() {
5153
Set<String> dropped = AnalyticsIndexConfig.stripUnsupportedMappingFields(json);
5254

5355
assertEquals(
54-
Set.of("nested_value", "geo_point_value", "geo_shape_value", "binary_value", "alias_value"),
55-
dropped);
56+
Set.of("nested_value", "geo_point_value", "geo_shape_value", "alias_value"), dropped);
5657

5758
JSONObject props = json.getJSONObject("mappings").getJSONObject("properties");
5859
assertTrue("supported scalar kept", props.has("keep_text"));
60+
assertTrue("binary is a supported scan type — must be kept", props.has("binary_value"));
5961
assertFalse(props.has("nested_value"));
6062
assertFalse(props.has("geo_point_value"));
6163
assertFalse(props.has("geo_shape_value"));
62-
assertFalse(props.has("binary_value"));
6364
assertFalse(props.has("alias_value"));
6465

6566
// object field kept, but its unsupported sub-property stripped recursively
@@ -82,12 +83,13 @@ public void bulkStrip_removesDroppedKeysFromSourceLinesOnly() {
8283
enable();
8384
String bulk =
8485
"{\"index\":{\"_id\":\"1\"}}\n"
85-
+ "{\"keep_text\":\"x\",\"geo_point_value\":{\"lat\":1,\"lon\":2},\"binary_value\":\"AA==\"}\n"
86+
+ "{\"keep_text\":\"x\",\"geo_point_value\":{\"lat\":1,\"lon\":2},\"geo_shape_value\":\"POINT(1"
87+
+ " 2)\"}\n"
8688
+ "{\"index\":{\"_id\":\"2\"}}\n"
8789
+ "{\"keep_text\":\"y\",\"nested_value\":[{\"a\":1}]}\n";
8890
String out =
8991
AnalyticsIndexConfig.stripBulkFields(
90-
bulk, Set.of("geo_point_value", "binary_value", "nested_value"));
92+
bulk, Set.of("geo_point_value", "geo_shape_value", "nested_value"));
9193

9294
String[] lines = out.split("\n");
9395
// action lines untouched
@@ -97,12 +99,33 @@ public void bulkStrip_removesDroppedKeysFromSourceLinesOnly() {
9799
JSONObject doc1 = new JSONObject(lines[1]);
98100
assertTrue(doc1.has("keep_text"));
99101
assertFalse(doc1.has("geo_point_value"));
100-
assertFalse(doc1.has("binary_value"));
102+
assertFalse(doc1.has("geo_shape_value"));
101103
JSONObject doc2 = new JSONObject(lines[3]);
102104
assertTrue(doc2.has("keep_text"));
103105
assertFalse(doc2.has("nested_value"));
104106
}
105107

108+
@Test
109+
public void bulkStrip_leavesUntouchedSourceLinesByteForByte() {
110+
enable();
111+
// doc1 carries a dropped key (gets rewritten); doc2 does not (must pass through verbatim).
112+
String docWithDrop = "{\"keep_text\":\"x\",\"nested_value\":[{\"a\":1}]}";
113+
String docNoDrop = "{\"keep_text\":\"y\",\"age\": 30,\"z\":1}";
114+
String bulk =
115+
"{\"index\":{\"_id\":\"1\"}}\n"
116+
+ docWithDrop
117+
+ "\n"
118+
+ "{\"index\":{\"_id\":\"2\"}}\n"
119+
+ docNoDrop
120+
+ "\n";
121+
String out = AnalyticsIndexConfig.stripBulkFields(bulk, Set.of("nested_value"));
122+
String[] lines = out.split("\n", -1);
123+
// The doc that had no dropped key is byte-for-byte identical (odd spacing/key order preserved).
124+
assertEquals(docNoDrop, lines[3]);
125+
// The doc that had a dropped key lost it.
126+
assertFalse(new JSONObject(lines[1]).has("nested_value"));
127+
}
128+
106129
@Test
107130
public void bulkStrip_noopWhenDisabledOrEmptyDropSet() {
108131
String bulk = "{\"index\":{}}\n{\"geo_point_value\":{\"lat\":1}}\n";

integ-test/src/test/java/org/opensearch/sql/legacy/TestUtils.java

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,34 @@ static String bulkLoadRefreshParam() {
146146
}
147147

148148
/**
149-
* Field types the analytics-engine (DataFusion) backend cannot read. Any property of one of
150-
* these types is removed from the mapping (and the matching key from each bulk doc) before a
151-
* test index is created/loaded on the analytics-engine route, so seeding a dataset that happens
152-
* to use one of them doesn't fail the whole test. Applied uniformly to every dataset — no
153-
* per-index variant files to author or keep in sync.
149+
* Field types the analytics-engine (DataFusion) backend cannot read, and which therefore must be
150+
* removed from a test index's mapping (and the matching key from each bulk doc) before the index
151+
* is created/loaded on the analytics-engine route. Seeding a dataset that happens to use one of
152+
* them would otherwise fail ingestion and surface as an unrelated {@code expected:<1> but
153+
* was:<0>} downstream — so we strip uniformly at load time across every dataset, no per-index
154+
* variant files to author or keep in sync.
155+
*
156+
* <p><b>This set is the single source of truth.</b> The strip triggers purely on a mapping's
157+
* field <em>types</em> (not on which IT is running), so any IT — existing or newly added — that
158+
* loads one of these datasets through {@link SQLIntegTestCase#loadIndex} is handled out of the
159+
* box. {@code AnalyticsUnsupportedFieldStripVerifyIT} imports this same constant and proves no
160+
* listed type survives in any live mapping, so the list cannot silently drift.
161+
*
162+
* <p><b>What is and isn't here, and why:</b>
163+
*
164+
* <ul>
165+
* <li>{@code nested} — entire subtree dropped (the engine has no nested-document support).
166+
* <li>{@code geo_point}, {@code geo_shape} — fall through the engine's type whitelist
167+
* ({@code OpenSearchSchemaBuilder.mapFieldType} default → null), so the column can't scan.
168+
* <li>{@code alias} — indirection the scan-row-type builder doesn't resolve.
169+
* <li><b>{@code binary} is intentionally NOT here</b> — the engine maps {@code binary →
170+
* VARBINARY} (same as {@code ip}, see {@code OpenSearchSchemaBuilder.mapFieldType}), so it
171+
* is a supported scan type. Stripping it would needlessly mutate fixtures and could mask a
172+
* real binary-handling bug rather than let the assertion surface it.
173+
* </ul>
154174
*/
155-
private static final Set<String> UNSUPPORTED_FIELD_TYPES =
156-
Set.of("nested", "geo_point", "geo_shape", "binary", "alias");
175+
public static final Set<String> UNSUPPORTED_FIELD_TYPES =
176+
Set.of("nested", "geo_point", "geo_shape", "alias");
157177

158178
/**
159179
* Remove every {@link #UNSUPPORTED_FIELD_TYPES} property (recursively, including object
@@ -209,6 +229,11 @@ private static boolean removeIfUnsupported(JSONObject properties, String field)
209229
* alternates an action line ({@code {"index":{...}}}) with a source line; only source lines
210230
* (those without a bulk action key) are rewritten. No-op when disabled or {@code droppedFields}
211231
* is empty.
232+
*
233+
* <p>A source line is re-serialized <em>only when it actually carried one of the dropped
234+
* keys</em>; every other line (action lines and docs that never had the field) is appended
235+
* byte-for-byte unchanged. This keeps the payload as close to the on-disk fixture as possible —
236+
* we don't reorder keys or reformat numbers on docs we aren't modifying.
212237
*/
213238
static String stripBulkFields(String bulkBody, Set<String> droppedFields) {
214239
if (!isEnabled() || droppedFields.isEmpty()) {
@@ -224,10 +249,18 @@ static String stripBulkFields(String bulkBody, Set<String> droppedFields) {
224249
boolean isActionLine =
225250
doc.has("index") || doc.has("create") || doc.has("update") || doc.has("delete");
226251
if (!isActionLine) {
252+
boolean removedAny = false;
227253
for (String f : droppedFields) {
228-
doc.remove(f);
254+
if (doc.has(f)) {
255+
doc.remove(f);
256+
removedAny = true;
257+
}
258+
}
259+
// Only rewrite the line if we actually removed something; otherwise leave it verbatim
260+
// so untouched docs stay byte-for-byte identical to the fixture.
261+
if (removedAny) {
262+
line = doc.toString();
229263
}
230-
line = doc.toString();
231264
}
232265
}
233266
out.append(line);

0 commit comments

Comments
 (0)