test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(integ-test): stabilize deterministic fixtures across shards - #5730

Open
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures
Open

test(integ-test): stabilize deterministic fixtures across shards#5730
mengweieric wants to merge 6 commits into
opensearch-project:mainfrom
mengweieric:menwe/multi-shard-deterministic-fixtures

Conversation

@mengweieric

Copy link
Copy Markdown
Collaborator

Summary

A set of integration tests selected rows through head, limit, shard-local representative choice, or unordered collection output, then asserted exact single-shard results.

This change selects intended fixture rows with unique keys, compares unordered collections as multisets, and uses numeric tolerances only for distributed floating-point or geo-point encoding differences. Schema, cardinality, grouping, source-value, and command-specific assertions remain exact.

No production behavior is modified.

Validation

  • Verified on an external cluster forced to five primary shards
  • Exercised direct, paginating, and independently checked no-pushdown paths as applicable
  • All modified tests passed targeted five-shard validation
  • spotlessCheck, compileTestJava, and git diff --check pass

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 <menwe@amazon.com>
@github-actions

github-actionsBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

(Review updated until commit 31f8a2a)

Here are some key observations to aid the review process:

🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ No major issues detected

Signed-off-by: Eric Wei <menwe@amazon.com>
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 60e3690

@github-actions

github-actionsBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to 31f8a2a

Explore these optional code suggestions:

CategorySuggestion Impact
General
Use tolerance for floating-point comparison

Comparing floating-point numbers with == can produce incorrect results due to
precision issues. Use a tolerance-based comparison (e.g., Math.abs(diff) < EPSILON)
to handle rounding errors properly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [171-181]

 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();+ double aVal = ((Number) a).doubleValue();+ double bVal = ((Number) b).doubleValue();+ return Math.abs(aVal - bVal) < 1e-9;
}
return a.toString().equals(b.toString());
}
Suggestion importance[1-10]: 7

__

Why: Valid concern about floating-point comparison precision. The suggestion to use tolerance-based comparison (Math.abs(aVal - bVal) < 1e-9) is appropriate for test assertions where rounding errors can occur. This improves robustness without changing the test's intent.

Medium
Use structural JSON comparison

Converting JSON objects to strings for comparison can produce false positives when
different JSON structures serialize to identical strings. Use a structural
comparison that respects JSON semantics instead of string equality.

integ-test/src/test/java/org/opensearch/sql/legacy/CursorIT.java [501-516]

 public void verifyDataRows(JSONArray dataRowsOne, JSONArray dataRowsTwo) {
...
- List<String> rowsOne = new ArrayList<>();- dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add(o.toString()));- List<String> rowsTwo = new ArrayList<>();- dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add(o.toString()));- rowsOne.sort(null);- rowsTwo.sort(null);- assertEquals(rowsOne, rowsTwo);+ List<JSONArray> rowsOne = new ArrayList<>();+ dataRowsOne.iterator().forEachRemaining(o -> rowsOne.add((JSONArray) o));+ List<JSONArray> rowsTwo = new ArrayList<>();+ dataRowsTwo.iterator().forEachRemaining(o -> rowsTwo.add((JSONArray) o));+ rowsOne.sort(Comparator.comparing(JSONArray::toString));+ rowsTwo.sort(Comparator.comparing(JSONArray::toString));+ assertEquals(rowsOne.size(), rowsTwo.size());+ for (int i = 0; i < rowsOne.size(); i++) {+ assertTrue(rowsOne.get(i).similar(rowsTwo.get(i)));+ }
}
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies that string comparison of JSON can be fragile. Using JSONArray.similar() for structural comparison is more robust. However, the implementation still sorts by toString() which could be problematic. A better approach would use a custom comparator that handles JSON structure, but the suggested improvement is still an upgrade over pure string equality.

Low
Optimize array comparison algorithm

The nested loop with iterator removal has O(n²) time complexity for array
comparison. For large arrays with many elements, this could cause performance
issues. Consider using a frequency map approach to achieve O(n) complexity while
still handling duplicates correctly.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [146-169]

 private static boolean jsonArrayEqualsIgnoringNestedOrder(JSONArray actual, JSONArray expected) {
if (actual.length() != expected.length()) {
return false;
}
- List<Object> remaining = new ArrayList<>();+ Map<String, Integer> expectedCounts = new HashMap<>();
for (int i = 0; i < expected.length(); i++) {
- remaining.add(expected.get(i));+ String key = expected.get(i).toString();+ expectedCounts.merge(key, 1, Integer::sum);
}
for (int i = 0; i < actual.length(); i++) {
- Object actualElement = actual.get(i);- boolean matched = false;- for (Iterator<Object> it = remaining.iterator(); it.hasNext(); ) {- if (jsonEqualsIgnoringOrder(actualElement, it.next())) {- it.remove();- matched = true;- break;- }- }- if (!matched) {+ String key = actual.get(i).toString();+ Integer count = expectedCounts.get(key);+ if (count == null || count == 0) {
return false;
}
+ expectedCounts.put(key, count - 1);
}
return true;
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies a performance concern with the O(n²) nested loop. However, the proposed frequency map approach using toString() as keys would break the deep structural comparison that jsonEqualsIgnoringOrder provides. The current implementation correctly handles nested arrays/objects recursively, while the suggested approach would only compare serialized strings, losing semantic correctness.

Low

Previous suggestions

Suggestions up to commit b9c4c8a
CategorySuggestion Impact
Possible issue
Handle type mismatches in recursive comparison

The recursive comparison does not handle the case where a and b are of different
types (e.g., one is a JSONArray and the other is a JSONObject). This will fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the recursive calls.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 private static boolean jsonEqualsIgnoringOrder(Object a, Object b) {
if (a instanceof JSONArray && b instanceof JSONArray) {
return jsonArrayEqualsIgnoringNestedOrder((JSONArray) a, (JSONArray) b);
+ }+ if (a instanceof JSONArray || b instanceof JSONArray) {+ return false;
}
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;
}
+ if (a instanceof JSONObject || b instanceof JSONObject) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that the method doesn't explicitly handle type mismatches between JSONArray and JSONObject before falling through to scalarEquals. Adding explicit checks prevents incorrect comparisons when a and b are of different JSON container types, improving correctness and clarity.

Medium
Suggestions up to commit 60e3690
CategorySuggestion Impact
General
Handle type mismatch in comparison

The method does not handle the case where a and b are of different types (e.g., one
is a JSONArray and the other is a JSONObject). This will cause the method to fall
through to scalarEquals, which may produce incorrect results. Add an explicit type
mismatch check before the scalar comparison.

integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLGraphLookupIT.java [116-134]

 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;
}
+ if ((a instanceof JSONArray || a instanceof JSONObject) != (b instanceof JSONArray || b instanceof JSONObject)) {+ return false;+ }
return scalarEquals(a, b);
}
Suggestion importance[1-10]: 3

__

Why: The suggestion correctly identifies that type mismatches between JSONArray and JSONObject are not explicitly handled before falling through to scalarEquals. However, the current implementation already handles this implicitly: if a and b are different types (one is JSONArray, the other is JSONObject), neither of the first two conditions will match, and scalarEquals will return false via toString().equals(). The explicit check adds clarity but doesn't fix a bug.

Low

Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit b9c4c8a

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"

@dai-chendai-chenSep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see most are caused by head and we fix by adding sort or where. Just thinking any way to enforce this in our IT?

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. OpenSearch core handles this behaviorally rather than with a static rule. Its integration-test framework randomizes index shard counts from 1 to 10, and tests choose ordered or order-insensitive assertions based on the contract. A static head rule would flag valid deterministic cases and miss non-head sources of shard-order dependence, so I opened #5738 to track a route-independent multi-shard SQL CI lane: #5738

Q10 selected its single top row with sort on an aggregated double alone,
so a tie would let head 1 pick either row. Sort on c_custkey as well to
pin the selected row on any shard layout.
Signed-off-by: Eric Wei <menwe@amazon.com>
A JSONArray or JSONObject on only one side fell through to a toString
comparison, so a container whose serialized form equalled a scalar
string matched incorrectly. Reject that case and cover it with tests.
Signed-off-by: Eric Wei <menwe@amazon.com>
@github-actions

Copy link
Copy Markdown
Contributor

Persistent review updated to latest commit 31f8a2a

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

testingRelated to improving software testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@mengweieric@dai-chen