diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDashboardPatternsIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDashboardPatternsIT.java
index 1ab5c7fc9bc..b20c0d32293 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDashboardPatternsIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDashboardPatternsIT.java
@@ -11,7 +11,6 @@
import static org.opensearch.sql.util.MatcherUtils.verifyDataRows;
import static org.opensearch.sql.util.MatcherUtils.verifySchemaInOrder;
-import com.google.common.collect.ImmutableList;
import java.io.IOException;
import org.json.JSONObject;
import org.junit.Test;
@@ -35,7 +34,7 @@ public void testDashboardBrainLabelStatsByPatternsField() throws IOException {
+ " | patterns content method=BRAIN mode=label"
+ " max_sample_count=5 variable_count_threshold=5"
+ " frequency_threshold_percentage=0.2"
- + " | stats count() as pattern_count, take(content, 1) as sample_logs"
+ + " | stats count() as pattern_count, max(content) as sample_logs"
+ " by patterns_field"
+ " | sort - pattern_count"
+ " | fields patterns_field, pattern_count, sample_logs",
@@ -44,31 +43,34 @@ public void testDashboardBrainLabelStatsByPatternsField() throws IOException {
result,
schema("patterns_field", "string"),
schema("pattern_count", "bigint"),
- schema("sample_logs", "array"));
+ schema("sample_logs", "string"));
+ // Each of the four BRAIN patterns covers exactly two documents (count 2, shard-invariant).
+ // max(content) picks the lexicographically-largest of a pattern's two source lines, which is
+ // order-insensitive and therefore stable across shards and routes, so the sample is exact
+ // rather than an arbitrary take(content, 1). Row order is order-insensitive here (all counts
+ // tie at 2).
verifyDataRows(
result,
rows(
"BLOCK* NameSystem.addStoredBlock: blockMap updated: <*IP*> is added to blk_<*> size"
+ " <*>",
2,
- ImmutableList.of(
- "BLOCK* NameSystem.addStoredBlock: blockMap updated: 10.251.31.85:50010 is added"
- + " to blk_-7017553867379051457 size 67108864")),
+ "BLOCK* NameSystem.addStoredBlock: blockMap updated: 10.251.31.85:50010 is added to"
+ + " blk_-7017553867379051457 size 67108864"),
rows(
"PacketResponder failed <*> blk_<*>",
2,
- ImmutableList.of("PacketResponder failed for blk_6996194389878584395")),
+ "PacketResponder failed for blk_6996194389878584395"),
rows(
"Verification succeeded <*> blk_<*>",
2,
- ImmutableList.of("Verification succeeded for blk_-1547954353065580372")),
+ "Verification succeeded for blk_6996194389878584395"),
rows(
"<*> NameSystem.allocateBlock:"
+ " /user/root/sortrand/_temporary/_task_<*>_<*>_r_<*>_<*>/part<*> blk_<*>",
2,
- ImmutableList.of(
- "BLOCK* NameSystem.allocateBlock:"
- + " /user/root/sortrand/_temporary/_task_200811092030_0002_r_000296_0/part-00296."
- + " blk_-6620182933895093708")));
+ "BLOCK* NameSystem.allocateBlock:"
+ + " /user/root/sortrand/_temporary/_task_200811092030_0002_r_000318_0/part-00318."
+ + " blk_2096692261399680562"));
}
}
diff --git a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java
index 4177d108440..db7333daaeb 100644
--- a/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java
+++ b/integ-test/src/test/java/org/opensearch/sql/calcite/remote/CalcitePPLDedupIT.java
@@ -7,10 +7,20 @@
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_ACCOUNT;
import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DUPLICATION_NULLABLE;
+import static org.opensearch.sql.legacy.TestsConstants.TEST_INDEX_DUPLICATION_NULLABLE_ORDERED;
import static org.opensearch.sql.util.Capability.DEDUP_NONDETERMINISTIC;
import static org.opensearch.sql.util.MatcherUtils.*;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.opensearch.sql.ppl.PPLIntegTestCase;
@@ -24,6 +34,7 @@ public void init() throws Exception {
enableCalcite();
loadIndex(Index.DUPLICATION_NULLABLE);
+ loadIndex(Index.DUPLICATION_NULLABLE_ORDERED);
loadIndex(Index.ACCOUNT);
}
@@ -55,15 +66,22 @@ public void testDedupMultipleFields() throws IOException {
@Test
public void testDedupKeepEmpty() throws IOException {
+ // dedup 1 name KEEPEMPTY=true keeps the first row per distinct non-null name plus every
+ // null-name row. An added `sort name, category` (PPL default ASC NULLS FIRST) pins each
+ // non-null name's surviving row to its smallest category on any shard layout and route -- the
+ // same sort-before-dedup determinism the #3922 regression tests rely on -- so the kept
+ // representative is exact rather than merely "some valid pair". Per name that is A->X,
+ // B->null, C->X, D->Z, E->null; the four null-name rows are always kept in full.
JSONObject actual =
executeQuery(
String.format(
- "source=%s | dedup 1 name KEEPEMPTY=true | fields name, category",
+ "source=%s | sort name, category | dedup 1 name KEEPEMPTY=true | fields name,"
+ + " category",
TEST_INDEX_DUPLICATION_NULLABLE));
verifyDataRows(
actual,
rows("A", "X"),
- rows("B", "Z"),
+ rows("B", null),
rows("C", "X"),
rows("D", "Z"),
rows("E", null),
@@ -96,6 +114,17 @@ public void testDedupMultipleFieldsKeepEmpty() throws IOException {
rows(null, null));
}
+ /**
+ * {@code CONSECUTIVE=true} collapses only adjacent duplicates, so its result depends
+ * entirely on the row-encounter order. A multi-shard index has no stable merge order (the counts
+ * observed on a single shard, 8/12/12/16, become 12/... on five shards). To keep the real index
+ * route while making the encounter order deterministic, this drives a seq-augmented fixture
+ * ({@code duplication_nullable_ordered}, same rows plus an explicit {@code seq}) and adds {@code
+ * | sort seq} before dedup, restoring the historical {@code duplication_nullable} insertion
+ * sequence on any shard layout while still exercising real CONSECUTIVE semantics over the index.
+ * The AE route has no stable per-fragment tiebreaker (DEDUP_NONDETERMINISTIC), so the assertion
+ * stays gated to the routes that produce a deterministic ordered stream.
+ */
@Test
@RequiresCapability(
value = DEDUP_NONDETERMINISTIC,
@@ -104,29 +133,31 @@ public void testConsecutiveImplicitFallbackV2() throws IOException {
JSONObject actual =
executeQuery(
String.format(
- "source = %s | dedup 1 name CONSECUTIVE=true | fields name",
- TEST_INDEX_DUPLICATION_NULLABLE));
+ "source = %s | sort seq | dedup 1 name CONSECUTIVE=true | fields name",
+ TEST_INDEX_DUPLICATION_NULLABLE_ORDERED));
verifyNumOfRows(actual, 8);
actual =
executeQuery(
String.format(
- "source = %s | dedup 1 name KEEPEMPTY=true CONSECUTIVE=true | fields name",
- TEST_INDEX_DUPLICATION_NULLABLE));
+ "source = %s | sort seq | dedup 1 name KEEPEMPTY=true CONSECUTIVE=true | fields"
+ + " name",
+ TEST_INDEX_DUPLICATION_NULLABLE_ORDERED));
verifyNumOfRows(actual, 12);
actual =
executeQuery(
String.format(
- "source = %s | dedup 2 name CONSECUTIVE=true | fields name",
- TEST_INDEX_DUPLICATION_NULLABLE));
+ "source = %s | sort seq | dedup 2 name CONSECUTIVE=true | fields name",
+ TEST_INDEX_DUPLICATION_NULLABLE_ORDERED));
verifyNumOfRows(actual, 12);
actual =
executeQuery(
String.format(
- "source = %s | dedup 2 name KEEPEMPTY=true CONSECUTIVE=true | fields name",
- TEST_INDEX_DUPLICATION_NULLABLE));
+ "source = %s | sort seq | dedup 2 name KEEPEMPTY=true CONSECUTIVE=true | fields"
+ + " name",
+ TEST_INDEX_DUPLICATION_NULLABLE_ORDERED));
verifyNumOfRows(actual, 16);
}
@@ -169,20 +200,25 @@ public void testDedupKeepEmpty2() throws IOException {
String.format(
"source=%s | dedup 2 name KEEPEMPTY=true | fields name, category",
TEST_INDEX_DUPLICATION_NULLABLE));
- verifyDataRows(
- actual,
- rows("A", "X"),
- rows("A", "Y"),
- rows("B", "Z"),
- rows("B", "Z"),
- rows("C", "X"),
- rows("C", "X"),
- rows("D", "Z"),
- rows("E", null),
- rows(null, "Y"),
- rows(null, "X"),
- rows(null, "Z"),
- rows(null, null));
+ // dedup 2 keeps up to two rows per distinct non-null name (A/B/C have >=2, D/E have 1) plus
+ // every null-name row. Which two categories survive per name has no stable cross-shard
+ // tiebreaker, so assert the per-name kept-count, valid pairs, and the fixed null-name rows.
+ List> rows = dataRows(actual);
+ assertEquals(12, rows.size());
+ Map