From a1ba64612e0030e075d8ece9d7eef4af6927fe9c Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Sat, 29 Aug 2026 03:54:25 +0000 Subject: [PATCH 1/3] test(integ-test): stabilize deterministic fixtures across shards Select fixture rows by unique keys, compare unordered collections as multisets, and use numeric tolerances where distributed accumulation or encoding is lossy. Keep exact schema, cardinality, and source-value assertions. Signed-off-by: Eric Wei --- .../sql/api/UnifiedQueryOpenSearchIT.java | 3 +- .../sql/calcite/CalciteComplexPoolIT.java | 2 +- .../remote/CalciteArrayFunctionIT.java | 4 +- .../calcite/remote/CalciteBinCommandIT.java | 8 +- .../remote/CalciteFieldsCommandIT.java | 14 ++- .../remote/CalciteMultisearchCommandIT.java | 11 +- .../remote/CalciteMvCombineCommandIT.java | 9 +- .../remote/CalcitePPLEnhancedCoalesceIT.java | 10 +- .../remote/CalcitePPLGraphLookupIT.java | 102 +++++++++++++++++- .../sql/calcite/remote/CalcitePPLJoinIT.java | 27 +++-- .../remote/CalciteTransposeCommandIT.java | 32 +++--- .../sql/calcite/tpch/CalcitePPLTpchIT.java | 8 +- .../org/opensearch/sql/legacy/CursorIT.java | 15 ++- .../opensearch/sql/ppl/GeoPointFormatsIT.java | 19 ++-- .../sql/ppl/MatchPhrasePrefixIT.java | 2 +- .../sql/ppl/MathematicalFunctionIT.java | 9 +- .../org/opensearch/sql/ppl/SettingsIT.java | 19 +++- .../org/opensearch/sql/sql/ConditionalIT.java | 2 +- .../sql/sql/DateTimeFunctionIT.java | 59 ++++++---- .../opensearch/sql/sql/GeopointFormatsIT.java | 22 ++-- .../org/opensearch/sql/sql/IdentifierIT.java | 18 +++- .../opensearch/sql/sql/WildcardQueryIT.java | 6 +- .../date_time_index_mapping.json | 3 + 23 files changed, 301 insertions(+), 103 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/api/UnifiedQueryOpenSearchIT.java b/integ-test/src/test/java/org/opensearch/sql/api/UnifiedQueryOpenSearchIT.java index df7557976f0..996d06c227a 100644 --- a/integ-test/src/test/java/org/opensearch/sql/api/UnifiedQueryOpenSearchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/api/UnifiedQueryOpenSearchIT.java @@ -72,7 +72,8 @@ public void cleanUp() throws Exception { public void testSimplePPLQueryExecution() throws Exception { String pplQuery = String.format( - "source = opensearch.%s | fields firstname, age | where age > 30 | head 3", + "source = opensearch.%s | where age > 30 and account_number in (1, 6, 18) | fields" + + " firstname, age", TEST_INDEX_ACCOUNT); RelNode logicalPlan = planner.plan(pplQuery); diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/CalciteComplexPoolIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/CalciteComplexPoolIT.java index b419c8043d3..3be25127b7e 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/CalciteComplexPoolIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/CalciteComplexPoolIT.java @@ -46,7 +46,7 @@ public void testParseCommandDispatchesToComplexPool() throws IOException { executeQuery( String.format( "source=%s | parse address '(?\\\\d+) (?.*)'" - + " | fields number, street | head 1", + + " | sort account_number | fields number, street | head 1", TEST_INDEX_BANK)); verifyDataRows(result, rows("880", "Holmes Lane")); diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java index 0f5b2bb5649..aa935ab6e56 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteArrayFunctionIT.java @@ -955,8 +955,8 @@ public void testMvmapWithOtherFieldReference() throws IOException { JSONObject actual = executeQuery( String.format( - "source=%s | eval arr = array(1, 2, 3), result = mvmap(arr, arr * age) | head 1 |" - + " fields age, result", + "source=%s | eval arr = array(1, 2, 3), result = mvmap(arr, arr * age) | sort" + + " account_number | head 1 | fields age, result", TEST_INDEX_BANK)); verifySchema(actual, schema("age", "int"), schema("result", "array")); diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteBinCommandIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteBinCommandIT.java index a04a34a0b41..c6d247699c0 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteBinCommandIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteBinCommandIT.java @@ -441,8 +441,8 @@ public void testBinTimestampSpan7Days() throws IOException { JSONObject result = executeQuery( String.format( - "source=%s | bin @timestamp span=7day | fields" - + " @timestamp, value | sort @timestamp | head 3", + "source=%s | eval original_timestamp = @timestamp | bin @timestamp span=7day |" + + " sort original_timestamp | head 3 | fields @timestamp, value", TEST_INDEX_TIME_DATA)); verifySchema(result, schema("@timestamp", null, "timestamp"), schema("value", null, "int")); verifyDataRows( @@ -457,8 +457,8 @@ public void testBinTimestampSpan6Days() throws IOException { JSONObject result = executeQuery( String.format( - "source=%s | bin @timestamp span=6day | fields" - + " @timestamp, value | sort @timestamp | head 3", + "source=%s | eval original_timestamp = @timestamp | bin @timestamp span=6day |" + + " sort original_timestamp | head 3 | fields @timestamp, value", TEST_INDEX_TIME_DATA)); verifySchema(result, schema("@timestamp", null, "timestamp"), schema("value", null, "int")); verifyDataRows( diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteFieldsCommandIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteFieldsCommandIT.java index 445ae4e7439..350a255b9d4 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteFieldsCommandIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteFieldsCommandIT.java @@ -573,10 +573,14 @@ public void testTableWithDuplicateWildcardMatches() throws IOException { public void testFieldsAndTableEquivalence() throws IOException { JSONObject fieldsResult = executeQuery( - String.format("source=%s | fields firstname, lastname | head 3", TEST_INDEX_ACCOUNT)); + String.format( + "source=%s | where account_number in (1, 6, 13) | fields firstname, lastname", + TEST_INDEX_ACCOUNT)); JSONObject tableResult = executeQuery( - String.format("source=%s | table firstname, lastname | head 3", TEST_INDEX_ACCOUNT)); + String.format( + "source=%s | where account_number in (1, 6, 13) | table firstname, lastname", + TEST_INDEX_ACCOUNT)); verifySchema(fieldsResult, schema("firstname", "string"), schema("lastname", "string")); verifySchema(tableResult, schema("firstname", "string"), schema("lastname", "string")); @@ -592,11 +596,13 @@ public void testSpaceDelimitedEquivalentToCommaDelimited() throws IOException { JSONObject commaResult = executeQuery( String.format( - "source=%s | fields firstname, lastname, age | head 3", TEST_INDEX_ACCOUNT)); + "source=%s | where account_number in (1, 6, 13) | fields firstname, lastname, age", + TEST_INDEX_ACCOUNT)); JSONObject spaceResult = executeQuery( String.format( - "source=%s | fields firstname lastname age | head 3", TEST_INDEX_ACCOUNT)); + "source=%s | where account_number in (1, 6, 13) | fields firstname lastname age", + TEST_INDEX_ACCOUNT)); verifySchema( commaResult, diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java index 383ae5e400f..6ded538e643 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMultisearchCommandIT.java @@ -129,9 +129,10 @@ public void testMultisearchWithFieldsProjection() throws IOException { JSONObject result = executeQuery( String.format( - "| multisearch [search source=%s | where gender = \\\"M\\\" | fields" - + " firstname, lastname, balance] [search source=%s | where gender = \\\"F\\\"" - + " | fields firstname, lastname, balance] | head 5", + "| multisearch [search source=%s | where gender = \\\"M\\\" and account_number in" + + " (1, 6, 18) | fields firstname, lastname, balance] [search source=%s | where" + + " gender = \\\"F\\\" and account_number in (13, 25) | fields firstname," + + " lastname, balance]", TEST_INDEX_ACCOUNT, TEST_INDEX_ACCOUNT)); verifySchema( @@ -145,8 +146,8 @@ public void testMultisearchWithFieldsProjection() throws IOException { rows("Amber", "Duke", 39225L), rows("Hattie", "Bond", 5686L), rows("Dale", "Adams", 4180L), - rows("Elinor", "Ratliff", 16418L), - rows("Mcgee", "Mooney", 18612L)); + rows("Nanette", "Bates", 32838L), + rows("Virginia", "Ayala", 40540L)); } @Test diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMvCombineCommandIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMvCombineCommandIT.java index 1cf535bd71b..c09e23ff7f6 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMvCombineCommandIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteMvCombineCommandIT.java @@ -68,7 +68,14 @@ public void testMvCombine_basicGroupCollapsesToOneRow() throws IOException { schema("tags", null, "string"), schema("packets_str", null, "array")); - verifyDataRows(result, rows("10.0.0.1", 100, "t1", List.of("10", "20", "30"))); + JSONArray row = result.getJSONArray("datarows").getJSONArray(0); + Assertions.assertEquals("10.0.0.1", row.getString(0)); + Assertions.assertEquals(100, row.getLong(1)); + Assertions.assertEquals("t1", row.getString(2)); + List packets = new ArrayList<>(); + row.getJSONArray(3).forEach(value -> packets.add(value.toString())); + Collections.sort(packets); + Assertions.assertEquals(List.of("10", "20", "30"), packets); } @Test diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLEnhancedCoalesceIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLEnhancedCoalesceIT.java index 35c99c92b42..99787070c3d 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLEnhancedCoalesceIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLEnhancedCoalesceIT.java @@ -44,8 +44,8 @@ public void testCoalesceBasic() throws IOException { JSONObject actual = executeQuery( String.format( - "source=%s | eval result = coalesce(name, age, 0) | fields name, age, result |" - + " head 3", + "source=%s | eval result = coalesce(name, age, 0) | sort - age | fields name, age," + + " result | head 3", TEST_INDEX_STATE_COUNTRY_WITH_NULL)); verifySchema( @@ -61,7 +61,7 @@ public void testCoalesceWithMixedTypes() throws IOException { executeQuery( String.format( "source=%s | eval result = coalesce(name, age, 'fallback') |" - + " fields name, age, result | head 3", + + " sort - age | fields name, age, result | head 3", TEST_INDEX_STATE_COUNTRY_WITH_NULL)); verifySchema( @@ -170,8 +170,8 @@ public void testCoalesceWithAllNonExistentFields() throws IOException { JSONObject actual = executeQuery( String.format( - "source=%s | eval result = coalesce(field1, field2, field3) | fields name, result |" - + " head 1", + "source=%s | eval result = coalesce(field1, field2, field3) | sort - age | fields" + + " name, result | head 1", TEST_INDEX_STATE_COUNTRY_WITH_NULL)); // When every COALESCE operand is missing/null, the result has no known type (see #5175). diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java index 62e872be945..f0b7865197a 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java @@ -14,10 +14,15 @@ import static org.opensearch.sql.util.MatcherUtils.verifySchema; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.Test; import org.opensearch.sql.ppl.PPLIntegTestCase; @@ -63,6 +68,91 @@ private static Map mapOf(Object... keysAndValues) { return map; } + /** + * Row matcher for graphLookup results whose collected arrays come back in a shard-dependent + * order. + * + *

graphLookup gathers traversal results into an array, and the order of elements within that + * array is not defined across shards. A multi-shard run therefore returns the same set of nodes + * (same values, same {@code depth}/{@code numConnections}, same cardinality) as a single-shard + * run, only in a different order. Unlike {@link + * org.opensearch.sql.util.MatcherUtils#rows(Object...)}, which compares row cells with + * order-sensitive {@link JSONArray#similar}, this matcher compares the row against the expected + * cells treating every nested JSON array as an unordered multiset. It is scoped to this test + * class so the relaxed comparison never leaks into other suites. + */ + private static TypeSafeMatcher rowsUnordered(Object... expectedObjects) { + return new TypeSafeMatcher<>() { + @Override + protected boolean matchesSafely(JSONArray array) { + return jsonArrayEqualsIgnoringNestedOrder(array, new JSONArray(expectedObjects)); + } + + @Override + public void describeTo(Description description) { + description.appendText(new JSONArray(expectedObjects).toString()); + } + }; + } + + /** Recursively compares two JSON values, treating every array as an unordered multiset. */ + private static boolean jsonEqualsIgnoringOrder(Object a, Object b) { + if (a instanceof JSONArray && b instanceof JSONArray) { + return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b); + } + if (a instanceof JSONObject && b instanceof JSONObject) { + JSONObject ao = (JSONObject) a; + JSONObject bo = (JSONObject) b; + if (ao.keySet().size() != bo.keySet().size()) { + return false; + } + for (String key : ao.keySet()) { + if (!bo.has(key) || !jsonEqualsIgnoringOrder(ao.get(key), bo.get(key))) { + return false; + } + } + return true; + } + return scalarEquals(a, b); + } + + private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) { + if (actual.length() != expected.length()) { + return false; + } + List remaining = new ArrayList<>(); + for (int i = 0; i < expected.length(); i++) { + remaining.add(expected.get(i)); + } + for (int i = 0; i < actual.length(); i++) { + Object actualElement = actual.get(i); + boolean matched = false; + for (Iterator it = remaining.iterator(); it.hasNext(); ) { + if (jsonEqualsIgnoringOrder(actualElement, it.next())) { + it.remove(); + matched = true; + break; + } + } + if (!matched) { + return false; + } + } + return true; + } + + private static boolean scalarEquals(Object a, Object b) { + boolean aNull = a == null || a == JSONObject.NULL; + boolean bNull = b == null || b == JSONObject.NULL; + if (aNull || bNull) { + return aNull && bNull; + } + if (a instanceof Number && b instanceof Number) { + return ((Number) a).doubleValue() == ((Number) b).doubleValue(); + } + return a.toString().equals(b.toString()); + } + // ==================== Employee Hierarchy Tests ==================== /** Test 1: Basic employee hierarchy traversal. Find all managers in the reporting chain. */ @@ -398,9 +488,11 @@ public void testTravelerReachableAirportsWithMaxDepth() throws IOException { schema("name", "string"), schema("nearestAirport", "string"), schema("reachableAirports", "array")); + // graphLookup collects reachable airports into an array whose element order is shard-dependent; + // compare values/connects/cardinality while ignoring nested array order. verifyDataRows( result, - rows( + rowsUnordered( "Jeff", "BOS", List.of( @@ -708,9 +800,11 @@ public void testBatchModeEmployeeHierarchy() throws IOException { TEST_INDEX_GRAPH_EMPLOYEES, TEST_INDEX_GRAPH_EMPLOYEES)); verifySchema(result, schema("reportsTo", "array"), schema("reportingHierarchy", "array")); + // Batch mode returns a single row; the source-rows array and the hierarchy array both come + // back in a shard-dependent order, so compare them as unordered multisets. verifyDataRows( result, - rows( + rowsUnordered( List.of( Map.of("name", "Dev", "reportsTo", "Eliot", "id", 1), Map.of("name", "Asya", "reportsTo", "Ron", "id", 5)), @@ -746,7 +840,7 @@ public void testBatchModeTravelersAirports() throws IOException { // - lookupResults: airports reachable from JFK and BOS within maxDepth=1 verifyDataRows( result, - rows( + rowsUnordered( List.of( Map.of("name", "Dev", "nearestAirport", "JFK"), Map.of("name", "Eliot", "nearestAirport", "JFK"), @@ -850,7 +944,7 @@ public void testTopLevelGraphLookupLiteralList() throws IOException { // because Ron.reportsTo=Andrew is in visited set verifyDataRows( result, - rows( + rowsUnordered( (Object) List.of( Map.of("name", "Eliot", "reportsTo", "Ron", "id", 2), diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java index cdefe15155c..b599a4ebdcc 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLJoinIT.java @@ -15,6 +15,7 @@ import static org.opensearch.sql.util.MatcherUtils.schema; import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; import static org.opensearch.sql.util.MatcherUtils.verifyDataRowsInOrder; +import static org.opensearch.sql.util.MatcherUtils.verifyDataRowsSome; import static org.opensearch.sql.util.MatcherUtils.verifyNumOfRows; import static org.opensearch.sql.util.MatcherUtils.verifySchema; @@ -898,13 +899,20 @@ public void testJoinWithFieldListMaxEqualsOne() throws IOException { String.format( "source=%s | join type=inner max=1 name,year,month %s | fields name, country", TEST_INDEX_STATE_COUNTRY, TEST_INDEX_OCCUPATION)); - verifyDataRows( + // max=1 keeps a single OCCUPATION match per left row, and the co-named country column resolves + // to the OCCUPATION (right) side (see Jake -> England). David has two OCCUPATION rows that are + // identical on the join field list (name=David, year=2023, month=4) and differ only in the + // projected country (USA from the Doctor row, Canada from the Unemployed row), so which one + // survives max=1 is undefined and cannot be disambiguated by any ordering. Assert the four + // deterministic rows and that max=1 collapses David's two matches to a single row (total of 5, + // not 6); do not pin David's undefined country. + verifyNumOfRows(actual2, 5); + verifyDataRowsSome( actual2, rows("Jake", "England"), rows("Jane", "Canada"), rows("John", "Canada"), - rows("Hello", "USA"), - rows("David", "USA")); + rows("Hello", "USA")); } @Test @@ -946,13 +954,20 @@ public void testJoinWhenLegacyNotPreferred() throws IOException { } catch (IOException e) { fail(); } - verifyDataRows( + // max=1 keeps a single OCCUPATION match per left row, and the co-named country column + // resolves to the OCCUPATION (right) side. David has two OCCUPATION rows identical on the + // join field list (name=David, year=2023, month=4) that differ only in the projected + // country (USA/Canada), so which one survives max=1 is undefined and cannot be + // disambiguated by any ordering. Assert the four deterministic rows and that max=1 + // collapses David's two matches to a single row (total of 5, not 6); do not pin David's + // undefined country. + verifyNumOfRows(actual2, 5); + verifyDataRowsSome( actual2, rows("Jake", "England"), rows("Jane", "Canada"), rows("John", "Canada"), - rows("Hello", "USA"), - rows("David", "USA")); + rows("Hello", "USA")); }); } diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteTransposeCommandIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteTransposeCommandIT.java index 676cf162b03..8302e8d6599 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteTransposeCommandIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalciteTransposeCommandIT.java @@ -18,6 +18,13 @@ public class CalciteTransposeCommandIT extends PPLIntegTestCase { + private static final String ACCOUNT_STREAM = + "makeresults format=csv data='firstname:string,age:int,balance:long\\n" + + "Amber,32,39225\\nHattie,36,5686\\nNanette,28,32838\\nDale,33,4180\\n" + + "Elinor,36,16418\\nVirginia,39,40540\\nDillard,34,48086\\nMcgee,39,18612\\n" + + "Aurelia,37,34487\\nFulton,23,29104\\nBurton,31,14097\\nJosie,32,14992\\n" + + "Hughes,30,6077\\nHall,25,44214\\nDeidre,33,38172'"; + @Override public void init() throws Exception { super.init(); @@ -33,11 +40,11 @@ public void init() throws Exception { */ @Test public void testTranspose() throws IOException { + // `head` selects rows by input encounter order, which is not stable across shards. Use the + // deterministic ACCOUNT_STREAM for exact head/transpose examples; other tests in this class + // retain the real index path for field-name collisions and aggregate inputs. var result = - executeQuery( - String.format( - "source=%s | head 5 | fields firstname, age, balance | transpose", - TEST_INDEX_ACCOUNT)); + executeQuery(ACCOUNT_STREAM + " | head 5 | fields firstname, age, balance | transpose"); // Verify that we get original rows plus totals row verifySchema( @@ -63,10 +70,7 @@ public void testTranspose() throws IOException { @Test public void testTransposeLimit() throws IOException { var result = - executeQuery( - String.format( - "source=%s | head 10 | fields firstname , age, balance | transpose 14", - TEST_INDEX_ACCOUNT)); + executeQuery(ACCOUNT_STREAM + " | head 10 | fields firstname, age, balance | transpose 14"); // Verify that we get original rows plus totals row verifySchema( @@ -119,10 +123,7 @@ public void testTransposeLimit() throws IOException { @Test public void testTransposeLowerLimit() throws IOException { var result = - executeQuery( - String.format( - "source=%s | head 15 | fields firstname , age, balance | transpose 5", - TEST_INDEX_ACCOUNT)); + executeQuery(ACCOUNT_STREAM + " | head 15 | fields firstname, age, balance | transpose 5"); // Verify that we get original rows plus totals row verifySchema( @@ -192,10 +193,9 @@ public void testTransposeWithValueFieldNameCollision() throws IOException { public void testTransposeColumnName() throws IOException { var result = executeQuery( - String.format( - "source=%s | head 5 | fields firstname, age, balance | transpose 5" - + " column_name='column_names'", - TEST_INDEX_ACCOUNT)); + ACCOUNT_STREAM + + " | head 5 | fields firstname, age, balance | transpose 5" + + " column_name='column_names'"); // Verify that we get original rows plus totals row verifySchema( diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/tpch/CalcitePPLTpchIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/tpch/CalcitePPLTpchIT.java index 0642b8f5651..4bed4cad729 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/tpch/CalcitePPLTpchIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/tpch/CalcitePPLTpchIT.java @@ -170,7 +170,7 @@ public void testQ6() throws IOException { String ppl = sanitize(loadFromFile("tpch/queries/q6.ppl")); JSONObject actual = executeQuery(ppl); verifySchemaInOrder(actual, schema("revenue", "double")); - verifyDataRows(actual, rows(77949.9186)); + verifyDataRows(actual, closeTo(77949.9186)); } @Test @@ -224,9 +224,13 @@ public void testQ10() throws IOException { schema("c_comment", "string")); verifyNumOfRows(actual, 20); actual = executeQuery(ppl + "| head 1"); + // `revenue` is a floating-point SUM whose accumulation order depends on how documents are + // partitioned across shards, so a multi-shard run yields the same value up to a tiny rounding + // difference (282635.1719 vs 282635.17189999996). Use closeTo to compare numerics within + // tolerance while still asserting the non-numeric columns exactly. verifyDataRows( actual, - rows( + closeTo( 121, "Customer#000000121", 282635.17189999996, diff --git a/integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java b/integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java index 0b0a510cb3d..dc4f87ddd2a 100644 --- a/integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java @@ -499,7 +499,20 @@ public void verifySchema(JSONArray schemaOne, JSONArray schemaTwo) { } public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) { - assertTrue(dataRowsOne.similar(dataRowsTwo)); + // The paginated and non-paginated APIs must return the same complete set of rows, but none of + // these queries impose a total ordering: three have no ORDER BY at all, and the ORDER BY + // variants sort on a non-unique column (balance), so the order among equal-key rows is not + // contractual. On a multi-shard index the scroll (paginated) and search (non-paginated) paths + // materialize the same rows in different orders, so JSONArray.similar()'s positional comparison + // fails even though the row sets are equivalent. Compare as multisets to assert complete-row + // equivalence (full coverage and totals) without depending on an order that is not contractual. + List rowsOne = new ArrayList<>(); + dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString())); + List rowsTwo = new ArrayList<>(); + dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString())); + rowsOne.sort(null); + rowsTwo.sort(null); + assertEquals(rowsOne, rowsTwo); } public String executeFetchAsStringQuery(String query, String fetchSize, String requestType) diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/GeoPointFormatsIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/GeoPointFormatsIT.java index 3f2c6ccbb62..a88f08e30b3 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/GeoPointFormatsIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/GeoPointFormatsIT.java @@ -34,13 +34,18 @@ public void testReadingGeopoints() throws IOException { executeQuery( String.format("search source=%s | head 5 | fields point ", TEST_INDEX_GEOPOINT)); verifySchema(result, schema("point", null, "geo_point")); - verifyDataRows( - result, - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71))); + // geo_point values are stored with Lucene's lossy 32-bit lat/lon encoding, so the decoded + // coordinates differ from the source (40.71, 74) by a sub-meter epsilon (e.g. 40.70999996736646 + // / 73.99999994784594). Assert each returned point within tolerance instead of exact equality. + // The five source documents all describe the same point in different formats, so any five rows + // selected by `head` satisfy this regardless of shard scan order. + JSONArray dataRows = result.getJSONArray("datarows"); + assertEquals(5, dataRows.length()); + for (int i = 0; i < dataRows.length(); i++) { + JSONObject point = ((JSONArray) dataRows.get(i)).getJSONObject(0); + assertEquals(40.71, point.getDouble("lat"), GeopointFormatsIT.TOLERANCE); + assertEquals(74, point.getDouble("lon"), GeopointFormatsIT.TOLERANCE); + } } @Test diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/MatchPhrasePrefixIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/MatchPhrasePrefixIT.java index 4023cc92d79..200e8fa2d70 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/MatchPhrasePrefixIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/MatchPhrasePrefixIT.java @@ -89,7 +89,7 @@ public void zero_term_query_all() throws IOException { // ORDER BY ... LIMIT helps make the test understandable. String query = "source = %s| WHERE match_phrase_prefix(Title, 'in to', analyzer=english," - + " zero_terms_query='ALL') | head 1 | fields Title"; + + " zero_terms_query='ALL') | sort Id | head 1 | fields Title"; JSONObject result = executeQuery(String.format(query, TEST_INDEX_BEER)); verifyDataRows(result, rows("How do you mull beer?")); } diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/MathematicalFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/MathematicalFunctionIT.java index 0a81ed92ad6..52c30ac5bf5 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/MathematicalFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/MathematicalFunctionIT.java @@ -736,7 +736,8 @@ public void testEvalComplexExpression() throws IOException { JSONObject result = executeQuery( String.format( - "source=%s | eval f = sum(age, 5) + avg(10, 20) | fields f | head 5", + "source=%s | sort account_number | eval f = sum(age, 5) + avg(10, 20) | fields f |" + + " head 5", TEST_INDEX_BANK)); verifySchema(result, schema("f", null, "double")); // sum(age, 5) + avg(10, 20) = (age + 5) + 15.0 @@ -760,7 +761,8 @@ public void testEvalSumWithMultipleFields() throws IOException { JSONObject result = executeQuery( String.format( - "source=%s | eval f = sum(age, age, 10) | fields f | head 5", TEST_INDEX_BANK)); + "source=%s | sort account_number | eval f = sum(age, age, 10) | fields f | head 5", + TEST_INDEX_BANK)); if (isCalciteEnabled()) { verifySchema(result, schema("f", null, "bigint")); } else { @@ -775,7 +777,8 @@ public void testEvalAvgWithExpression() throws IOException { JSONObject result = executeQuery( String.format( - "source=%s | eval f = avg(age * 2, 10) | fields f | head 5", TEST_INDEX_BANK)); + "source=%s | sort account_number | eval f = avg(age * 2, 10) | fields f | head 5", + TEST_INDEX_BANK)); verifySchema(result, schema("f", null, "double")); // avg(age * 2, 10) = (age * 2 + 10) / 2 = age + 5 verifyDataRows(result, rows(37.0), rows(41.0), rows(33.0), rows(38.0), rows(41.0)); diff --git a/integ-test/src/test/java/org/opensearch/sql/ppl/SettingsIT.java b/integ-test/src/test/java/org/opensearch/sql/ppl/SettingsIT.java index 16e1e1a532b..90dd4c93a6e 100644 --- a/integ-test/src/test/java/org/opensearch/sql/ppl/SettingsIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/ppl/SettingsIT.java @@ -25,13 +25,19 @@ public void init() throws Exception { public void testQuerySizeLimit() throws IOException { // Default setting, fetch 200 rows from query JSONObject result = - executeQuery(String.format("search source=%s age>35 | fields firstname", TEST_INDEX_BANK)); + executeQuery( + String.format( + "search source=%s age>35 | sort account_number | fields firstname", + TEST_INDEX_BANK)); verifyDataRows(result, rows("Hattie"), rows("Elinor"), rows("Virginia")); // Fetch 1 rows from query setQuerySizeLimit(1); result = - executeQuery(String.format("search source=%s age>35 | fields firstname", TEST_INDEX_BANK)); + executeQuery( + String.format( + "search source=%s age>35 | sort account_number | fields firstname", + TEST_INDEX_BANK)); verifyDataRows(result, rows("Hattie")); } @@ -41,7 +47,8 @@ public void testQuerySizeLimit_NoPushdown() throws IOException { JSONObject result = executeQuery( String.format( - "search source=%s | eval a = 1 | where age>35 | fields firstname", + "search source=%s | eval a = 1 | where age>35 | sort account_number | fields" + + " firstname", TEST_INDEX_BANK)); verifyDataRows(result, rows("Hattie"), rows("Elinor"), rows("Virginia")); @@ -50,7 +57,8 @@ public void testQuerySizeLimit_NoPushdown() throws IOException { result = executeQuery( String.format( - "search source=%s | eval a = 1 | where age>35 | fields firstname", + "search source=%s | eval a = 1 | where age>35 | sort account_number | fields" + + " firstname", TEST_INDEX_BANK)); verifyDataRows(result, rows("Hattie"), rows("Elinor")); @@ -59,7 +67,8 @@ public void testQuerySizeLimit_NoPushdown() throws IOException { result = executeQuery( String.format( - "search source=%s | eval a = 1 | where age>35 | fields firstname", + "search source=%s | eval a = 1 | where age>35 | sort account_number | fields" + + " firstname", TEST_INDEX_BANK)); verifyDataRows(result, rows("Hattie")); } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java index 88c1bd54ce0..b79e04bc5f4 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/ConditionalIT.java @@ -119,7 +119,7 @@ public void nullifWithNotNullInputTestOne() { "SELECT NULLIF(firstname, 'Amber JOHnny') as testnullif " + "FROM " + TEST_INDEX_BANK_WITH_NULL_VALUES - + " limit 2 ", + + " ORDER BY account_number limit 2 ", "jdbc")); verifySchema(response, schema("NULLIF(firstname, 'Amber JOHnny')", "testnullif", "keyword")); verifyDataRows(response, rows(LITERAL_NULL.value()), rows("Hattie")); diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java index 2435256fcb5..0989481fc6b 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/DateTimeFunctionIT.java @@ -506,8 +506,8 @@ public void testExtractWithDatetime() throws IOException { JSONObject datetimeResult = executeQuery( String.format( - "SELECT extract(DAY_SECOND FROM timestamp(cast(datetime0 AS STRING))) FROM %s LIMIT" - + " 1", + "SELECT extract(DAY_SECOND FROM timestamp(cast(datetime0 AS STRING))) FROM %s ORDER" + + " BY `key` LIMIT 1", TEST_INDEX_CALCS)); verifyDataRows(datetimeResult, rows(9101735)); } @@ -517,7 +517,8 @@ public void testExtractWithTime() throws IOException { JSONObject timeResult = executeQuery( String.format( - "SELECT extract(HOUR_SECOND FROM time0) FROM %s LIMIT 1", TEST_INDEX_CALCS)); + "SELECT extract(HOUR_SECOND FROM time0) FROM %s ORDER BY `key` LIMIT 1", + TEST_INDEX_CALCS)); verifyDataRows(timeResult, rows(210732)); } @@ -526,7 +527,8 @@ public void testExtractWithDate() throws IOException { JSONObject dateResult = executeQuery( String.format( - "SELECT extract(YEAR_MONTH FROM date0) FROM %s LIMIT 1", TEST_INDEX_CALCS)); + "SELECT extract(YEAR_MONTH FROM date0) FROM %s ORDER BY `key` LIMIT 1", + TEST_INDEX_CALCS)); verifyDataRows(dateResult, rows(200404)); } @@ -579,15 +581,21 @@ public void testHourFunctionAliasesReturnTheSameResults() throws IOException { @Test public void testLastDay() throws IOException { JSONObject result = - executeQuery(String.format("SELECT last_day(date0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + executeQuery( + String.format( + "SELECT last_day(date0) FROM %s ORDER BY `key` LIMIT 3", TEST_INDEX_CALCS)); verifyDataRows(result, rows("2004-04-30"), rows("1972-07-31"), rows("1975-11-30")); result = - executeQuery(String.format("SELECT last_day(date0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + executeQuery( + String.format( + "SELECT last_day(date0) FROM %s ORDER BY `key` LIMIT 3", TEST_INDEX_CALCS)); verifyDataRows(result, rows("2004-04-30"), rows("1972-07-31"), rows("1975-11-30")); result = - executeQuery(String.format("SELECT last_day(date0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + executeQuery( + String.format( + "SELECT last_day(date0) FROM %s ORDER BY `key` LIMIT 3", TEST_INDEX_CALCS)); verifyDataRows(result, rows("2004-04-30"), rows("1972-07-31"), rows("1975-11-30")); } @@ -822,7 +830,10 @@ public void testQuarter() throws IOException { @Test public void testSecToTime() throws IOException { JSONObject result = - executeQuery(String.format("SELECT sec_to_time(balance) FROM %s LIMIT 3", TEST_INDEX_BANK)); + executeQuery( + String.format( + "SELECT sec_to_time(balance) FROM %s ORDER BY account_number LIMIT 3", + TEST_INDEX_BANK)); verifyDataRows(result, rows("10:53:45"), rows("01:34:46"), rows("09:07:18")); } @@ -904,7 +915,7 @@ public void testStrToDate() throws IOException { executeQuery( String.format( "SELECT str_to_date(CAST(birthdate AS STRING)," - + " '%%Y-%%m-%%d %%h:%%i:%%s') FROM %s LIMIT 2", + + " '%%Y-%%m-%%d %%h:%%i:%%s') FROM %s ORDER BY account_number LIMIT 2", TEST_INDEX_BANK)); verifyDataRows(result, rows("2017-10-23 00:00:00"), rows("2017-11-20 00:00:00")); @@ -912,7 +923,8 @@ public void testStrToDate() throws IOException { result = executeQuery( String.format( - "SELECT str_to_date(CAST(birthdate AS STRING)," + " '%%Y %%s') FROM %s LIMIT 2", + "SELECT str_to_date(CAST(birthdate AS STRING)," + + " '%%Y %%s') FROM %s ORDER BY account_number LIMIT 2", TEST_INDEX_BANK)); verifyDataRows(result, rows((Object) null), rows((Object) null)); @@ -920,7 +932,8 @@ public void testStrToDate() throws IOException { result = executeQuery( String.format( - "SELECT str_to_date(firstname," + " '%%Y-%%m-%%d %%h:%%i:%%s') FROM %s LIMIT 2", + "SELECT str_to_date(firstname," + + " '%%Y-%%m-%%d %%h:%%i:%%s') FROM %s ORDER BY account_number LIMIT 2", TEST_INDEX_BANK)); verifyDataRows(result, rows((Object) null), rows((Object) null)); @@ -997,7 +1010,9 @@ public void testSubDateWithInterval() throws IOException { public void testTimstampadd() throws IOException { JSONObject result = executeQuery( - String.format("SELECT timestampadd(WEEK, 2, time0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + String.format( + "SELECT timestampadd(WEEK, 2, time0) FROM %s ORDER BY `key` LIMIT 3", + TEST_INDEX_CALCS)); verifyDataRows( result, @@ -1011,7 +1026,8 @@ public void testTimstampdiff() throws IOException { JSONObject result = executeQuery( String.format( - "SELECT timestampdiff(DAY, time0, datetime0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + "SELECT timestampdiff(DAY, time0, datetime0) FROM %s ORDER BY `key` LIMIT 3", + TEST_INDEX_CALCS)); verifyDataRows(result, rows(38176), rows(38191), rows(38198)); } @@ -1041,19 +1057,23 @@ public void testToDays() throws IOException { @Test public void testToSeconds() throws IOException { JSONObject result = - executeQuery(String.format("select to_seconds(date0) FROM %s LIMIT 2", TEST_INDEX_CALCS)); + executeQuery( + String.format( + "select to_seconds(date0) FROM %s ORDER BY `key` LIMIT 2", TEST_INDEX_CALCS)); verifyDataRows(result, rows(63249206400L), rows(62246275200L)); result = executeQuery( String.format( - "SELECT to_seconds(timestamp(cast(datetime0 AS string))) FROM %s LIMIT 2", + "SELECT to_seconds(timestamp(cast(datetime0 AS string))) FROM %s ORDER BY `key`" + + " LIMIT 2", TEST_INDEX_CALCS)); verifyDataRows(result, rows(63256587455L), rows(63258064234L)); result = executeQuery( - String.format("select to_seconds(datetime0) FROM %s LIMIT 2", TEST_INDEX_CALCS)); + String.format( + "select to_seconds(datetime0) FROM %s ORDER BY `key` LIMIT 2", TEST_INDEX_CALCS)); verifyDataRows(result, rows(63256587455L), rows(63258064234L)); } @@ -1095,7 +1115,9 @@ public void testWeek() throws IOException { @Test public void testWeekday() throws IOException { JSONObject result = - executeQuery(String.format("SELECT weekday(date0) FROM %s LIMIT 3", TEST_INDEX_CALCS)); + executeQuery( + String.format( + "SELECT weekday(date0) FROM %s ORDER BY `key` LIMIT 3", TEST_INDEX_CALCS)); verifyDataRows(result, rows(3), rows(1), rows(2)); } @@ -1154,7 +1176,8 @@ public void testYearweek() throws IOException { JSONObject result = executeQuery( String.format( - "SELECT yearweek(time0), yearweek(time0, 4) FROM %s LIMIT 2", TEST_INDEX_CALCS)); + "SELECT yearweek(time0), yearweek(time0, 4) FROM %s ORDER BY `key` LIMIT 2", + TEST_INDEX_CALCS)); verifyDataRows(result, rows(189952, 189952), rows(189953, 190001)); } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/GeopointFormatsIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/GeopointFormatsIT.java index fc79618fc07..1659fe2dbed 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/GeopointFormatsIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/GeopointFormatsIT.java @@ -6,13 +6,10 @@ package org.opensearch.sql.sql; import static org.opensearch.sql.util.Capability.GEOPOINT_TYPE; -import static org.opensearch.sql.util.MatcherUtils.rows; import static org.opensearch.sql.util.MatcherUtils.schema; -import static org.opensearch.sql.util.MatcherUtils.verifyDataRows; import static org.opensearch.sql.util.MatcherUtils.verifySchema; import java.io.IOException; -import java.util.Map; import org.apache.commons.lang3.tuple.Pair; import org.json.JSONArray; import org.json.JSONObject; @@ -33,13 +30,18 @@ public void testReadingGeopoints() throws IOException { String query = String.format("SELECT point FROM %s LIMIT 5", Index.GEOPOINTS.getName()); JSONObject result = executeJdbcRequest(query); verifySchema(result, schema("point", null, "geo_point")); - verifyDataRows( - result, - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71)), - rows(Map.of("lon", 74, "lat", 40.71))); + // geo_point values are stored with Lucene's lossy 32-bit lat/lon encoding, so the decoded + // coordinates differ from the source (40.71, 74) by a sub-meter epsilon. Assert each returned + // point within tolerance instead of exact equality. The five source documents all describe the + // same point in different formats, so any five rows selected by LIMIT satisfy this regardless + // of shard scan order. + JSONArray dataRows = result.getJSONArray("datarows"); + assertEquals(5, dataRows.length()); + for (int i = 0; i < dataRows.length(); i++) { + JSONObject point = ((JSONArray) dataRows.get(i)).getJSONObject(0); + assertEquals(40.71, point.getDouble("lat"), TOLERANCE); + assertEquals(74, point.getDouble("lon"), TOLERANCE); + } } public static final double TOLERANCE = 1E-5; diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/IdentifierIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/IdentifierIT.java index 50ddc0324d2..57851aee9bf 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/IdentifierIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/IdentifierIT.java @@ -15,6 +15,10 @@ import static org.opensearch.sql.util.TestUtils.performRequest; import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import org.json.JSONArray; import org.json.JSONObject; import org.junit.jupiter.api.Test; import org.opensearch.client.Request; @@ -130,11 +134,19 @@ public void testMetafieldIdentifierRoutingSelectTest() throws IOException { var datarows = result.getJSONArray("datarows"); assertEquals(6, datarows.length()); + // The returned row order is not stable across a multi-shard index (each doc is routed to a + // different shard), so sort rows by their unique _id metadata value before asserting. + List sortedRows = new ArrayList<>(); + for (int i = 0; i < datarows.length(); i++) { + sortedRows.add(datarows.getJSONArray(i)); + } + sortedRows.sort(Comparator.comparing((JSONArray row) -> row.getString(1))); + // note that _routing in the SELECT clause returns the shard for (int i = 0; i < 6; i++) { - assertEquals("test" + i, datarows.getJSONArray(i).getString(1)); - assertEquals(index, datarows.getJSONArray(i).getString(2)); - assertTrue(datarows.getJSONArray(i).getString(3).contains("[" + index + "]")); + assertEquals("test" + i, sortedRows.get(i).getString(1)); + assertEquals(index, sortedRows.get(i).getString(2)); + assertTrue(sortedRows.get(i).getString(3).contains("[" + index + "]")); } } diff --git a/integ-test/src/test/java/org/opensearch/sql/sql/WildcardQueryIT.java b/integ-test/src/test/java/org/opensearch/sql/sql/WildcardQueryIT.java index 8123f887f23..17606987f12 100644 --- a/integ-test/src/test/java/org/opensearch/sql/sql/WildcardQueryIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/sql/WildcardQueryIT.java @@ -22,19 +22,19 @@ protected void init() throws Exception { @Test public void test_wildcard_query_asterisk_function() throws IOException { - String expected = "test wildcard"; + String expected = "test backslash wildcard \\_"; String query1 = "SELECT KeywordBody FROM " + TEST_INDEX_WILDCARD - + " WHERE wildcard_query(KeywordBody, 't*') LIMIT 1"; + + " WHERE wildcard_query(KeywordBody, 't*') ORDER BY KeywordBody LIMIT 1"; JSONObject result1 = executeJdbcRequest(query1); verifyDataRows(result1, rows(expected)); String query2 = "SELECT KeywordBody FROM " + TEST_INDEX_WILDCARD - + " WHERE wildcardquery(KeywordBody, 't*') LIMIT 1"; + + " WHERE wildcardquery(KeywordBody, 't*') ORDER BY KeywordBody LIMIT 1"; JSONObject result2 = executeJdbcRequest(query2); verifyDataRows(result2, rows(expected)); } diff --git a/integ-test/src/test/resources/indexDefinitions/date_time_index_mapping.json b/integ-test/src/test/resources/indexDefinitions/date_time_index_mapping.json index 6f1d465a823..968ff31d6e3 100644 --- a/integ-test/src/test/resources/indexDefinitions/date_time_index_mapping.json +++ b/integ-test/src/test/resources/indexDefinitions/date_time_index_mapping.json @@ -3,6 +3,9 @@ "properties": { "birthday": { "type": "date" + }, + "login_time": { + "type": "date" } } } From 63acead2951d43f2637662bb22bdc935ad2673a9 Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Mon, 31 Aug 2026 18:30:42 +0000 Subject: [PATCH 2/3] test(integ-test): preserve graphLookup column positions Signed-off-by: Eric Wei --- .../remote/CalcitePPLGraphLookupIT.java | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java index f0b7865197a..9e93bd98e55 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java @@ -5,6 +5,8 @@ package org.opensearch.sql.calcite.remote; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_GRAPH_AIRPORTS; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_GRAPH_EMPLOYEES; import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_GRAPH_TRAVELERS; @@ -21,6 +23,7 @@ import java.util.List; import java.util.Map; import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.json.JSONArray; import org.json.JSONObject; @@ -77,15 +80,29 @@ private static Map mapOf(Object... keysAndValues) { * (same values, same {@code depth}/{@code numConnections}, same cardinality) as a single-shard * run, only in a different order. Unlike {@link * org.opensearch.sql.util.MatcherUtils#rows(Object...)}, which compares row cells with - * order-sensitive {@link JSONArray#similar}, this matcher compares the row against the expected - * cells treating every nested JSON array as an unordered multiset. It is scoped to this test - * class so the relaxed comparison never leaks into other suites. + * order-sensitive {@link JSONArray#similar}, this matcher relaxes ordering only within nested + * arrays and objects. The top-level row cells are still compared positionally: cell {@code + * i} of the actual row must match cell {@code i} of the expected row. This preserves column + * identity, so a swap of two same-typed top-level columns is still rejected, while a shard + * -dependent reorder inside a collected array is tolerated. It is scoped to this test class so + * the relaxed comparison never leaks into other suites. */ private static TypeSafeMatcher rowsUnordered(Object... expectedObjects) { return new TypeSafeMatcher<>() { @Override protected boolean matchesSafely(JSONArray array) { - return jsonArrayEqualsIgnoringNestedOrder(array, new JSONArray(expectedObjects)); + JSONArray expected = new JSONArray(expectedObjects); + if (array.length() != expected.length()) { + return false; + } + // Compare top-level cells positionally so column identity is preserved; only descend into + // nested arrays/objects with order-insensitive comparison. + for (int i = 0; i < expected.length(); i++) { + if (!jsonEqualsIgnoringOrder(array.get(i), expected.get(i))) { + return false; + } + } + return true; } @Override @@ -153,6 +170,28 @@ private static boolean scalarEquals(Object a, Object b) { return a.toString().equals(b.toString()); } + /** + * Regression test for {@link #rowsUnordered(Object...)}. Proves the matcher relaxes ordering only + * inside nested arrays/objects while keeping top-level column positions and cardinality + * significant. This runs without a cluster so it always executes as part of the suite. + */ + @Test + public void testRowsUnorderedMatcherPreservesColumnPositions() { + // 1. A shard-dependent reorder inside a nested collected array still matches. + Matcher matcher = rowsUnordered("Jeff", List.of("BOS", "JFK", "LAX")); + JSONArray nestedReordered = new JSONArray(List.of("Jeff", List.of("LAX", "BOS", "JFK"))); + assertTrue("nested array reorder should match", matcher.matches(nestedReordered)); + + // 2. A swap of two same-typed top-level columns must NOT match (column identity preserved). + Matcher swapMatcher = rowsUnordered("BOS", "JFK"); + JSONArray swapped = new JSONArray(List.of("JFK", "BOS")); + assertFalse("same-type top-level column swap must not match", swapMatcher.matches(swapped)); + + // 3. A missing value inside the nested array (different cardinality) must NOT match. + JSONArray missingNested = new JSONArray(List.of("Jeff", List.of("BOS", "JFK"))); + assertFalse("missing nested element must not match", matcher.matches(missingNested)); + } + // ==================== Employee Hierarchy Tests ==================== /** Test 1: Basic employee hierarchy traversal. Find all managers in the reporting chain. */ From 60e36901b9dfb7825d0ceb6c4b1814ff5ec8f5da Mon Sep 17 00:00:00 2001 From: Eric Wei Date: Mon, 31 Aug 2026 18:32:54 +0000 Subject: [PATCH 3/3] test(integ-test): clarify graphLookup matcher scope Signed-off-by: Eric Wei --- .../sql/calcite/remote/CalcitePPLGraphLookupIT.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java index 9e93bd98e55..69517b281db 100644 --- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java +++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java @@ -83,9 +83,9 @@ private static Map mapOf(Object... keysAndValues) { * order-sensitive {@link JSONArray#similar}, this matcher relaxes ordering only within nested * arrays and objects. The top-level row cells are still compared positionally: cell {@code * i} of the actual row must match cell {@code i} of the expected row. This preserves column - * identity, so a swap of two same-typed top-level columns is still rejected, while a shard - * -dependent reorder inside a collected array is tolerated. It is scoped to this test class so - * the relaxed comparison never leaks into other suites. + * identity, so a swap of two same-typed top-level columns is still rejected, while collected + * array order may vary across shards. It is scoped to this test class so the relaxed comparison + * never leaks into other suites. */ private static TypeSafeMatcher rowsUnordered(Object... expectedObjects) { return new TypeSafeMatcher<>() {